fix bug about sending playlist pictures
This commit is contained in:
parent
c678b60b69
commit
af5b20d3e6
File diff suppressed because one or more lines are too long
2
dist/index.html
vendored
2
dist/index.html
vendored
|
|
@ -6,7 +6,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>💕 Our Musical Journey</title>
|
||||
<meta name="description" content="A private musical journey for two hearts connected through music" />
|
||||
<script type="module" crossorigin src="/assets/index-BHCNYw42.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BailLCJ0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-HYQ6lVkA.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
BIN
frontend-dev.out
BIN
frontend-dev.out
Binary file not shown.
2
preview-3443.out
Normal file
2
preview-3443.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
(node:612693) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated
|
||||
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||
1261
server-dev.out
1261
server-dev.out
File diff suppressed because it is too large
Load Diff
|
|
@ -1 +1 @@
|
|||
592323
|
||||
619472
|
||||
|
|
|
|||
176
server/dist/lib/spotify.js
vendored
176
server/dist/lib/spotify.js
vendored
|
|
@ -6,28 +6,37 @@ import path from 'path';
|
|||
function generateRandomPlaylistImage() {
|
||||
try {
|
||||
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
|
||||
console.log(`[PLAYLIST COVER] Looking for images in directory: ${coversDir}`);
|
||||
// Check if directory exists
|
||||
if (!fs.existsSync(coversDir)) {
|
||||
console.log('Playlist covers directory not found, creating it...');
|
||||
console.log('[PLAYLIST COVER] Directory not found, creating it...');
|
||||
fs.mkdirSync(coversDir, { recursive: true });
|
||||
}
|
||||
// Get all image files from the directory
|
||||
// Get all image files from the directory (Spotify only accepts JPEG for cover uploads)
|
||||
const files = fs.readdirSync(coversDir).filter(file => {
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
return ['.jpg', '.jpeg', '.png', '.webp', '.gif'].includes(ext);
|
||||
return ['.jpg', '.jpeg'].includes(ext); // Only JPEG files for Spotify upload
|
||||
});
|
||||
console.log(`[PLAYLIST COVER] Found ${files.length} image files:`, files);
|
||||
// Log file sizes for debugging
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(coversDir, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
const sizeKB = (stats.size / 1024).toFixed(2);
|
||||
console.log(`[PLAYLIST COVER] File: ${file}, Size: ${sizeKB} KB`);
|
||||
});
|
||||
if (files.length === 0) {
|
||||
console.log('No playlist cover images found in public/playlist-covers/');
|
||||
console.log('[PLAYLIST COVER] No playlist cover images found in public/playlist-covers/');
|
||||
return '/api/placeholder-playlist-cover';
|
||||
}
|
||||
// Select a random image
|
||||
const randomFile = files[Math.floor(Math.random() * files.length)];
|
||||
const imageUrl = `/api/playlist-covers/${encodeURIComponent(randomFile)}`;
|
||||
console.log(`Selected random playlist cover: ${imageUrl}`);
|
||||
console.log(`[PLAYLIST COVER] Selected random playlist cover: ${imageUrl} (from file: ${randomFile})`);
|
||||
return imageUrl;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error generating random playlist image:', error);
|
||||
console.error('[PLAYLIST COVER] Error generating random playlist image:', error);
|
||||
return '/api/placeholder-playlist-cover';
|
||||
}
|
||||
}
|
||||
|
|
@ -94,10 +103,43 @@ export async function fetchRecommendations(accessToken, seeds, targets, limit =
|
|||
return fetchSimilarTrackUris(accessToken, seeds.seed_tracks, seeds.seed_artists, seeds.seed_genres, targets, limit);
|
||||
}
|
||||
export async function createSpotifyPlaylist(accessToken, userId, name, description) {
|
||||
console.log(`[PLAYLIST CREATION] Creating Spotify playlist: "${name}" for user: ${userId}`);
|
||||
const resp = await axios.post(`https://api.spotify.com/v1/users/${encodeURIComponent(userId)}/playlists`, { name, description, public: false }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } });
|
||||
const pl = resp.data;
|
||||
console.log(`[PLAYLIST CREATION] Spotify playlist created successfully with ID: ${pl.id}`);
|
||||
// Generate a random local playlist cover image
|
||||
const imageUrl = generateRandomPlaylistImage();
|
||||
// Extract the filename from the imageUrl to use the SAME image for Spotify upload
|
||||
const selectedFilename = decodeURIComponent(imageUrl.replace('/api/playlist-covers/', ''));
|
||||
console.log(`[PLAYLIST COVER] Selected image for both website and Spotify: ${selectedFilename}`);
|
||||
// Get the actual file path for uploading to Spotify (only JPEG files)
|
||||
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
|
||||
const files = fs.readdirSync(coversDir).filter(file => {
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
return ['.jpg', '.jpeg'].includes(ext); // Only JPEG files for Spotify upload
|
||||
});
|
||||
if (files.length > 0) {
|
||||
// Find the specific file that was selected for the website
|
||||
const selectedFile = files.find(file => file === selectedFilename);
|
||||
if (selectedFile) {
|
||||
const imagePath = path.join(coversDir, selectedFile);
|
||||
const stats = fs.statSync(imagePath);
|
||||
const fileSizeKB = (stats.size / 1024).toFixed(2);
|
||||
console.log(`[PLAYLIST COVER] Using SAME image for Spotify upload: ${selectedFile} (${fileSizeKB} KB)`);
|
||||
// Upload the SAME cover image to Spotify
|
||||
const uploadSuccess = await uploadPlaylistCover(accessToken, pl.id, imagePath);
|
||||
if (uploadSuccess) {
|
||||
console.log(`[PLAYLIST COVER] ✅ Successfully uploaded SAME cover image to Spotify playlist ${pl.id}`);
|
||||
}
|
||||
else {
|
||||
console.log(`[PLAYLIST COVER] ❌ Failed to upload ${selectedFile} to Spotify, but website will still show it`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(`[PLAYLIST COVER] ⚠️ Could not find selected file ${selectedFilename} for Spotify upload`);
|
||||
console.log(`[PLAYLIST COVER] ✅ WEBSITE COVER: Random cover image will still be displayed on your website.`);
|
||||
}
|
||||
}
|
||||
return { id: pl.id, url: pl.external_urls?.spotify, imageUrl };
|
||||
}
|
||||
export async function addTracks(accessToken, playlistId, trackUris) {
|
||||
|
|
@ -105,6 +147,128 @@ export async function addTracks(accessToken, playlistId, trackUris) {
|
|||
return;
|
||||
await axios.post(`https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/tracks`, { uris: trackUris }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } });
|
||||
}
|
||||
// Upload a playlist cover image to Spotify
|
||||
export async function uploadPlaylistCover(accessToken, playlistId, imagePath) {
|
||||
try {
|
||||
console.log(`[PLAYLIST COVER] Attempting to upload cover image for playlist ${playlistId} from path: ${imagePath}`);
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(imagePath)) {
|
||||
console.error(`[PLAYLIST COVER] Image file not found: ${imagePath}`);
|
||||
return false;
|
||||
}
|
||||
// Read the image file
|
||||
let imageBuffer = fs.readFileSync(imagePath);
|
||||
let fileSize = imageBuffer.length;
|
||||
console.log(`[PLAYLIST COVER] Original image file size: ${fileSize} bytes (${(fileSize / 1024).toFixed(2)} KB)`);
|
||||
// Check if we need to compress the image
|
||||
// We need to account for base64 encoding overhead (33% increase)
|
||||
// So we want the original file to be under ~190 KB to stay under 256 KB when base64 encoded
|
||||
const maxOriginalSize = 190 * 1024; // 190 KB
|
||||
if (fileSize > maxOriginalSize) {
|
||||
console.log(`[PLAYLIST COVER] Image too large (${(fileSize / 1024).toFixed(2)} KB), attempting to compress...`);
|
||||
// For now, let's skip files that are too large and try a different one
|
||||
console.error(`[PLAYLIST COVER] Image file too large for Spotify upload: ${fileSize} bytes (max ~190 KB to account for base64 overhead)`);
|
||||
return false;
|
||||
}
|
||||
// Convert to base64
|
||||
let base64Image = imageBuffer.toString('base64');
|
||||
const base64Size = base64Image.length;
|
||||
console.log(`[PLAYLIST COVER] Base64 length: ${base64Size} characters (${(base64Size / 1024).toFixed(2)} KB)`);
|
||||
// Ensure we don't have any data URL prefix
|
||||
if (base64Image.startsWith('data:')) {
|
||||
base64Image = base64Image.split(',')[1] || base64Image;
|
||||
console.log(`[PLAYLIST COVER] Removed data URL prefix from base64`);
|
||||
}
|
||||
// Final check - ensure base64 size is reasonable
|
||||
if (base64Size > 300 * 1024) { // 300 KB base64 limit
|
||||
console.error(`[PLAYLIST COVER] Base64 encoded image too large: ${base64Size} characters`);
|
||||
return false;
|
||||
}
|
||||
// Determine content type based on file extension
|
||||
const ext = path.extname(imagePath).toLowerCase();
|
||||
let contentType = 'image/jpeg'; // default
|
||||
switch (ext) {
|
||||
case '.png':
|
||||
contentType = 'image/png';
|
||||
break;
|
||||
case '.gif':
|
||||
contentType = 'image/gif';
|
||||
break;
|
||||
case '.webp':
|
||||
contentType = 'image/webp';
|
||||
break;
|
||||
case '.svg':
|
||||
contentType = 'image/svg+xml';
|
||||
break;
|
||||
case '.jpg':
|
||||
case '.jpeg':
|
||||
contentType = 'image/jpeg';
|
||||
break;
|
||||
}
|
||||
console.log(`[PLAYLIST COVER] Using content type: ${contentType}`);
|
||||
// Check token scopes first
|
||||
try {
|
||||
console.log(`[PLAYLIST COVER] Checking token scopes...`);
|
||||
const meResponse = await axios.get('https://api.spotify.com/v1/me', {
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||||
});
|
||||
console.log(`[PLAYLIST COVER] Token is valid for user: ${meResponse.data.display_name}`);
|
||||
// Test if we can access the playlist
|
||||
const playlistResponse = await axios.get(`https://api.spotify.com/v1/playlists/${playlistId}`, {
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||||
});
|
||||
console.log(`[PLAYLIST COVER] Can access playlist: ${playlistResponse.data.name}`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`[PLAYLIST COVER] Token validation failed:`, error.response?.data || error.message);
|
||||
}
|
||||
// Upload to Spotify
|
||||
console.log(`[PLAYLIST COVER] Making request to Spotify API: https://api.spotify.com/v1/playlists/${playlistId}/images`);
|
||||
console.log(`[PLAYLIST COVER] Access token (first 20 chars): ${accessToken.substring(0, 20)}...`);
|
||||
// Try different approaches for image upload
|
||||
let response;
|
||||
try {
|
||||
// Method 1: Standard PUT request (requires ugc-image-upload scope)
|
||||
response = await axios.put(`https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/images`, base64Image, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': contentType
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
console.log(`[PLAYLIST COVER] Method 1 failed with 401, trying alternative approach...`);
|
||||
// Method 2: Try with different content type
|
||||
try {
|
||||
response = await axios.put(`https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/images`, base64Image, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'image/jpeg' // Force JPEG content type
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error2) {
|
||||
console.log(`[PLAYLIST COVER] Method 2 also failed: ${error2.response?.status}`);
|
||||
throw error; // Re-throw original error
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw error; // Re-throw if not 401
|
||||
}
|
||||
}
|
||||
console.log(`[PLAYLIST COVER] Successfully uploaded cover image for playlist ${playlistId}, response status: ${response.status}`);
|
||||
console.log(`[PLAYLIST COVER] Response headers:`, response.headers);
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`[PLAYLIST COVER] Failed to upload cover image for playlist ${playlistId}:`);
|
||||
console.error(`[PLAYLIST COVER] Error status:`, error.response?.status);
|
||||
console.error(`[PLAYLIST COVER] Error data:`, error.response?.data);
|
||||
console.error(`[PLAYLIST COVER] Error message:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// ----- New: Similar tracks without recommendations endpoint -----
|
||||
async function getRelatedArtists(accessToken, artistId) {
|
||||
const resp = await axios.get(`https://api.spotify.com/v1/artists/${encodeURIComponent(artistId)}/related-artists`, {
|
||||
|
|
|
|||
34
server/dist/routes/playlists.js
vendored
34
server/dist/routes/playlists.js
vendored
|
|
@ -54,16 +54,21 @@ playlistsRouter.post('/mixed', requireAuth, async (req, res) => {
|
|||
try {
|
||||
const creatorId = req.user.uid;
|
||||
const { name, description, vibe, genres, includeKnown = true, partnerId, createForBoth = false, limit = 25 } = req.body || {};
|
||||
console.log(`[MIXED PLAYLIST] Starting playlist creation process for creator: ${creatorId}, partner: ${partnerId}`);
|
||||
console.log(`[MIXED PLAYLIST] Request parameters:`, { name, description, vibe, genres, includeKnown, createForBoth, limit });
|
||||
if (!partnerId)
|
||||
return res.status(400).json({ error: 'partnerId required' });
|
||||
// Collect seeds from both users' top tracks
|
||||
console.log(`[MIXED PLAYLIST] Collecting seed tracks from creator (${creatorId}) and partner (${partnerId})`);
|
||||
const seedRows1 = db.db.prepare('SELECT track_json FROM top_tracks WHERE user_id=? AND time_range=? ORDER BY rank ASC LIMIT 20').all(creatorId, 'short_term');
|
||||
const seedRows2 = db.db.prepare('SELECT track_json FROM top_tracks WHERE user_id=? AND time_range=? ORDER BY rank ASC LIMIT 20').all(partnerId, 'short_term');
|
||||
const tracks1 = seedRows1.map((r) => JSON.parse(r.track_json));
|
||||
const tracks2 = seedRows2.map((r) => JSON.parse(r.track_json));
|
||||
console.log(`[MIXED PLAYLIST] Found ${tracks1.length} tracks from creator, ${tracks2.length} tracks from partner`);
|
||||
let seedTrackUris = [...tracks1, ...tracks2]
|
||||
.map((t) => t.uri || (t.id ? `spotify:track:${t.id}` : null))
|
||||
.filter((u) => !!u);
|
||||
console.log(`[MIXED PLAYLIST] Total seed track URIs: ${seedTrackUris.length}`);
|
||||
// Fallback to artist seeds if track seeds are empty
|
||||
let seedArtistIds = [];
|
||||
if (seedTrackUris.length === 0) {
|
||||
|
|
@ -123,36 +128,45 @@ playlistsRouter.post('/mixed', requireAuth, async (req, res) => {
|
|||
const playlistName = name || generateUniquePlaylistName();
|
||||
const playlistDesc = description || 'An AI-blended mix with fresh recommendations';
|
||||
// Always create for the creator
|
||||
console.log(`[MIXED PLAYLIST] Creating Spotify playlist for creator: "${playlistName}"`);
|
||||
const creatorProfile = db.db.prepare('SELECT id FROM users WHERE id=?').get(creatorId);
|
||||
const { id: creatorPlaylistId, url: creatorUrl, imageUrl: creatorImageUrl } = await createSpotifyPlaylist(creatorToken, creatorProfile.id, playlistName, playlistDesc);
|
||||
console.log(`[MIXED PLAYLIST] Adding ${finalUris.length} tracks to creator playlist ${creatorPlaylistId}`);
|
||||
await addTracks(creatorToken, creatorPlaylistId, finalUris);
|
||||
console.log(`[MIXED PLAYLIST] Successfully added tracks to creator playlist`);
|
||||
let partnerResult = {};
|
||||
if (createForBoth) {
|
||||
console.log(`[MIXED PLAYLIST] Creating playlist for partner as well: ${partnerId}`);
|
||||
try {
|
||||
const partnerToken = await ensureValidAccessToken(partnerId);
|
||||
const partnerProfile = db.db.prepare('SELECT id FROM users WHERE id=?').get(partnerId);
|
||||
if (!partnerProfile) {
|
||||
throw new Error('Partner user not found');
|
||||
}
|
||||
console.log(`[MIXED PLAYLIST] Creating Spotify playlist for partner: "${playlistName}"`);
|
||||
const partnerPlaylist = await createSpotifyPlaylist(partnerToken, partnerProfile.id, playlistName, playlistDesc);
|
||||
partnerResult = partnerPlaylist;
|
||||
console.log(`[MIXED PLAYLIST] Adding ${finalUris.length} tracks to partner playlist ${partnerResult.id}`);
|
||||
await addTracks(partnerToken, partnerResult.id, finalUris);
|
||||
console.log(`Successfully created partner playlist: ${partnerResult.id}`);
|
||||
console.log(`[MIXED PLAYLIST] Successfully created partner playlist: ${partnerResult.id}`);
|
||||
}
|
||||
catch (e) {
|
||||
// Partner creation may fail (revoked consent, user not found, etc.); still return creator playlist
|
||||
const errorMessage = e?.response?.data?.error?.message || e?.message || 'Unknown error';
|
||||
console.error('Partner playlist creation failed:', errorMessage);
|
||||
console.error('[MIXED PLAYLIST] Partner playlist creation failed:', errorMessage);
|
||||
partnerResult.error = errorMessage;
|
||||
}
|
||||
}
|
||||
// Save playlist to database
|
||||
const playlistId = uuidv4();
|
||||
const now = Date.now();
|
||||
console.log('Saving playlist to database:', {
|
||||
console.log('[MIXED PLAYLIST] Saving playlist to database:', {
|
||||
id: playlistId,
|
||||
name: playlistName,
|
||||
trackCount: finalUris.length,
|
||||
creatorPlaylistId,
|
||||
partnerPlaylistId: partnerResult.id,
|
||||
imageUrl: creatorImageUrl,
|
||||
trackUris: finalUris.slice(0, 3) // Log first 3 URIs for debugging
|
||||
});
|
||||
db.db.prepare(`
|
||||
|
|
@ -163,7 +177,7 @@ playlistsRouter.post('/mixed', requireAuth, async (req, res) => {
|
|||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(playlistId, creatorId, partnerId, playlistName, playlistDesc, vibe || null, JSON.stringify(genres || []), JSON.stringify(finalUris), creatorPlaylistId, partnerResult.id || null, creatorUrl, partnerResult.url || null, creatorImageUrl || null, partnerResult.imageUrl || null, now, now);
|
||||
res.json({
|
||||
const response = {
|
||||
id: playlistId,
|
||||
name: playlistName,
|
||||
description: playlistDesc,
|
||||
|
|
@ -175,11 +189,21 @@ playlistsRouter.post('/mixed', requireAuth, async (req, res) => {
|
|||
partnerError: partnerResult.error || null,
|
||||
},
|
||||
added: finalUris.length,
|
||||
};
|
||||
console.log('[MIXED PLAYLIST] Playlist creation completed successfully:', {
|
||||
id: playlistId,
|
||||
name: playlistName,
|
||||
creatorPlaylistId,
|
||||
partnerPlaylistId: partnerResult.id,
|
||||
trackCount: finalUris.length,
|
||||
imageUrl: creatorImageUrl
|
||||
});
|
||||
res.json(response);
|
||||
}
|
||||
catch (err) {
|
||||
const detail = err?.response?.data || err?.message || 'Unknown error';
|
||||
console.error('Mixed playlist error:', detail);
|
||||
console.error('[MIXED PLAYLIST] Error during playlist creation:', detail);
|
||||
console.error('[MIXED PLAYLIST] Full error object:', err);
|
||||
res.status(500).json({ error: 'Failed to create mixed playlist', detail });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
38
server/node_modules/.package-lock.json
generated
vendored
38
server/node_modules/.package-lock.json
generated
vendored
|
|
@ -46,6 +46,22 @@
|
|||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz",
|
||||
"integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz",
|
||||
|
|
@ -68,6 +84,28 @@
|
|||
"@img/sharp-libvips-linux-x64": "1.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz",
|
||||
"integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "7.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||
|
|
|
|||
46
server/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md
generated
vendored
Normal file
46
server/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# `@img/sharp-libvips-linuxmusl-x64`
|
||||
|
||||
Prebuilt libvips and dependencies for use with sharp on Linux (musl) x64.
|
||||
|
||||
## Licensing
|
||||
|
||||
This software contains third-party libraries
|
||||
used under the terms of the following licences:
|
||||
|
||||
| Library | Used under the terms of |
|
||||
|---------------|-----------------------------------------------------------------------------------------------------------|
|
||||
| aom | BSD 2-Clause + [Alliance for Open Media Patent License 1.0](https://aomedia.org/license/patent-license/) |
|
||||
| cairo | Mozilla Public License 2.0 |
|
||||
| cgif | MIT Licence |
|
||||
| expat | MIT Licence |
|
||||
| fontconfig | [fontconfig Licence](https://gitlab.freedesktop.org/fontconfig/fontconfig/blob/main/COPYING) (BSD-like) |
|
||||
| freetype | [freetype Licence](https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT) (BSD-like) |
|
||||
| fribidi | LGPLv3 |
|
||||
| glib | LGPLv3 |
|
||||
| harfbuzz | MIT Licence |
|
||||
| highway | Apache-2.0 License, BSD 3-Clause |
|
||||
| lcms | MIT Licence |
|
||||
| libarchive | BSD 2-Clause |
|
||||
| libexif | LGPLv3 |
|
||||
| libffi | MIT Licence |
|
||||
| libheif | LGPLv3 |
|
||||
| libimagequant | [BSD 2-Clause](https://github.com/lovell/libimagequant/blob/main/COPYRIGHT) |
|
||||
| libnsgif | MIT Licence |
|
||||
| libpng | [libpng License](https://github.com/pnggroup/libpng/blob/master/LICENSE) |
|
||||
| librsvg | LGPLv3 |
|
||||
| libspng | [BSD 2-Clause, libpng License](https://github.com/randy408/libspng/blob/master/LICENSE) |
|
||||
| libtiff | [libtiff License](https://gitlab.com/libtiff/libtiff/blob/master/LICENSE.md) (BSD-like) |
|
||||
| libvips | LGPLv3 |
|
||||
| libwebp | New BSD License |
|
||||
| libxml2 | MIT Licence |
|
||||
| mozjpeg | [zlib License, IJG License, BSD-3-Clause](https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md) |
|
||||
| pango | LGPLv3 |
|
||||
| pixman | MIT Licence |
|
||||
| proxy-libintl | LGPLv3 |
|
||||
| zlib-ng | [zlib Licence](https://github.com/zlib-ng/zlib-ng/blob/develop/LICENSE.md) |
|
||||
|
||||
Use of libraries under the terms of the LGPLv3 is via the
|
||||
"any later version" clause of the LGPLv2 or LGPLv2.1.
|
||||
|
||||
Please report any errors or omissions via
|
||||
https://github.com/lovell/sharp-libvips/issues/new
|
||||
221
server/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h
generated
vendored
Normal file
221
server/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h
generated
vendored
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/* glibconfig.h
|
||||
*
|
||||
* This is a generated file. Please modify 'glibconfig.h.in'
|
||||
*/
|
||||
|
||||
#ifndef __GLIBCONFIG_H__
|
||||
#define __GLIBCONFIG_H__
|
||||
|
||||
#include <glib/gmacros.h>
|
||||
|
||||
#include <limits.h>
|
||||
#include <float.h>
|
||||
#define GLIB_HAVE_ALLOCA_H
|
||||
|
||||
#define GLIB_STATIC_COMPILATION 1
|
||||
#define GOBJECT_STATIC_COMPILATION 1
|
||||
#define GIO_STATIC_COMPILATION 1
|
||||
#define GMODULE_STATIC_COMPILATION 1
|
||||
#define GI_STATIC_COMPILATION 1
|
||||
#define G_INTL_STATIC_COMPILATION 1
|
||||
#define FFI_STATIC_BUILD 1
|
||||
|
||||
/* Specifies that GLib's g_print*() functions wrap the
|
||||
* system printf functions. This is useful to know, for example,
|
||||
* when using glibc's register_printf_function().
|
||||
*/
|
||||
#define GLIB_USING_SYSTEM_PRINTF
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define G_MINFLOAT FLT_MIN
|
||||
#define G_MAXFLOAT FLT_MAX
|
||||
#define G_MINDOUBLE DBL_MIN
|
||||
#define G_MAXDOUBLE DBL_MAX
|
||||
#define G_MINSHORT SHRT_MIN
|
||||
#define G_MAXSHORT SHRT_MAX
|
||||
#define G_MAXUSHORT USHRT_MAX
|
||||
#define G_MININT INT_MIN
|
||||
#define G_MAXINT INT_MAX
|
||||
#define G_MAXUINT UINT_MAX
|
||||
#define G_MINLONG LONG_MIN
|
||||
#define G_MAXLONG LONG_MAX
|
||||
#define G_MAXULONG ULONG_MAX
|
||||
|
||||
typedef signed char gint8;
|
||||
typedef unsigned char guint8;
|
||||
|
||||
typedef signed short gint16;
|
||||
typedef unsigned short guint16;
|
||||
|
||||
#define G_GINT16_MODIFIER "h"
|
||||
#define G_GINT16_FORMAT "hi"
|
||||
#define G_GUINT16_FORMAT "hu"
|
||||
|
||||
|
||||
typedef signed int gint32;
|
||||
typedef unsigned int guint32;
|
||||
|
||||
#define G_GINT32_MODIFIER ""
|
||||
#define G_GINT32_FORMAT "i"
|
||||
#define G_GUINT32_FORMAT "u"
|
||||
|
||||
|
||||
#define G_HAVE_GINT64 1 /* deprecated, always true */
|
||||
|
||||
typedef signed long gint64;
|
||||
typedef unsigned long guint64;
|
||||
|
||||
#define G_GINT64_CONSTANT(val) (val##L)
|
||||
#define G_GUINT64_CONSTANT(val) (val##UL)
|
||||
|
||||
#define G_GINT64_MODIFIER "l"
|
||||
#define G_GINT64_FORMAT "li"
|
||||
#define G_GUINT64_FORMAT "lu"
|
||||
|
||||
|
||||
#define GLIB_SIZEOF_VOID_P 8
|
||||
#define GLIB_SIZEOF_LONG 8
|
||||
#define GLIB_SIZEOF_SIZE_T 8
|
||||
#define GLIB_SIZEOF_SSIZE_T 8
|
||||
|
||||
typedef signed long gssize;
|
||||
typedef unsigned long gsize;
|
||||
#define G_GSIZE_MODIFIER "l"
|
||||
#define G_GSSIZE_MODIFIER "l"
|
||||
#define G_GSIZE_FORMAT "lu"
|
||||
#define G_GSSIZE_FORMAT "li"
|
||||
|
||||
#define G_MAXSIZE G_MAXULONG
|
||||
#define G_MINSSIZE G_MINLONG
|
||||
#define G_MAXSSIZE G_MAXLONG
|
||||
|
||||
typedef gint64 goffset;
|
||||
#define G_MINOFFSET G_MININT64
|
||||
#define G_MAXOFFSET G_MAXINT64
|
||||
|
||||
#define G_GOFFSET_MODIFIER G_GINT64_MODIFIER
|
||||
#define G_GOFFSET_FORMAT G_GINT64_FORMAT
|
||||
#define G_GOFFSET_CONSTANT(val) G_GINT64_CONSTANT(val)
|
||||
|
||||
#define G_POLLFD_FORMAT "%d"
|
||||
|
||||
#define GPOINTER_TO_INT(p) ((gint) (glong) (p))
|
||||
#define GPOINTER_TO_UINT(p) ((guint) (gulong) (p))
|
||||
|
||||
#define GINT_TO_POINTER(i) ((gpointer) (glong) (i))
|
||||
#define GUINT_TO_POINTER(u) ((gpointer) (gulong) (u))
|
||||
|
||||
typedef signed long gintptr;
|
||||
typedef unsigned long guintptr;
|
||||
|
||||
#define G_GINTPTR_MODIFIER "l"
|
||||
#define G_GINTPTR_FORMAT "li"
|
||||
#define G_GUINTPTR_FORMAT "lu"
|
||||
|
||||
#define GLIB_MAJOR_VERSION 2
|
||||
#define GLIB_MINOR_VERSION 86
|
||||
#define GLIB_MICRO_VERSION 0
|
||||
|
||||
#define G_OS_UNIX
|
||||
|
||||
#define G_VA_COPY va_copy
|
||||
|
||||
#define G_VA_COPY_AS_ARRAY 1
|
||||
|
||||
#define G_HAVE_ISO_VARARGS 1
|
||||
|
||||
/* gcc-2.95.x supports both gnu style and ISO varargs, but if -ansi
|
||||
* is passed ISO vararg support is turned off, and there is no work
|
||||
* around to turn it on, so we unconditionally turn it off.
|
||||
*/
|
||||
#if __GNUC__ == 2 && __GNUC_MINOR__ == 95
|
||||
# undef G_HAVE_ISO_VARARGS
|
||||
#endif
|
||||
|
||||
#define G_HAVE_GROWING_STACK 0
|
||||
|
||||
#ifndef _MSC_VER
|
||||
# define G_HAVE_GNUC_VARARGS 1
|
||||
#endif
|
||||
|
||||
#if defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590)
|
||||
#define G_GNUC_INTERNAL __attribute__((visibility("hidden")))
|
||||
#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x550)
|
||||
#define G_GNUC_INTERNAL __hidden
|
||||
#elif defined (__GNUC__) && defined (G_HAVE_GNUC_VISIBILITY)
|
||||
#define G_GNUC_INTERNAL __attribute__((visibility("hidden")))
|
||||
#else
|
||||
#define G_GNUC_INTERNAL
|
||||
#endif
|
||||
|
||||
#define G_THREADS_ENABLED
|
||||
#define G_THREADS_IMPL_POSIX
|
||||
|
||||
#define G_ATOMIC_LOCK_FREE
|
||||
|
||||
#define GINT16_TO_LE(val) ((gint16) (val))
|
||||
#define GUINT16_TO_LE(val) ((guint16) (val))
|
||||
#define GINT16_TO_BE(val) ((gint16) GUINT16_SWAP_LE_BE (val))
|
||||
#define GUINT16_TO_BE(val) (GUINT16_SWAP_LE_BE (val))
|
||||
|
||||
#define GINT32_TO_LE(val) ((gint32) (val))
|
||||
#define GUINT32_TO_LE(val) ((guint32) (val))
|
||||
#define GINT32_TO_BE(val) ((gint32) GUINT32_SWAP_LE_BE (val))
|
||||
#define GUINT32_TO_BE(val) (GUINT32_SWAP_LE_BE (val))
|
||||
|
||||
#define GINT64_TO_LE(val) ((gint64) (val))
|
||||
#define GUINT64_TO_LE(val) ((guint64) (val))
|
||||
#define GINT64_TO_BE(val) ((gint64) GUINT64_SWAP_LE_BE (val))
|
||||
#define GUINT64_TO_BE(val) (GUINT64_SWAP_LE_BE (val))
|
||||
|
||||
#define GLONG_TO_LE(val) ((glong) GINT64_TO_LE (val))
|
||||
#define GULONG_TO_LE(val) ((gulong) GUINT64_TO_LE (val))
|
||||
#define GLONG_TO_BE(val) ((glong) GINT64_TO_BE (val))
|
||||
#define GULONG_TO_BE(val) ((gulong) GUINT64_TO_BE (val))
|
||||
#define GINT_TO_LE(val) ((gint) GINT32_TO_LE (val))
|
||||
#define GUINT_TO_LE(val) ((guint) GUINT32_TO_LE (val))
|
||||
#define GINT_TO_BE(val) ((gint) GINT32_TO_BE (val))
|
||||
#define GUINT_TO_BE(val) ((guint) GUINT32_TO_BE (val))
|
||||
#define GSIZE_TO_LE(val) ((gsize) GUINT64_TO_LE (val))
|
||||
#define GSSIZE_TO_LE(val) ((gssize) GINT64_TO_LE (val))
|
||||
#define GSIZE_TO_BE(val) ((gsize) GUINT64_TO_BE (val))
|
||||
#define GSSIZE_TO_BE(val) ((gssize) GINT64_TO_BE (val))
|
||||
#define G_BYTE_ORDER G_LITTLE_ENDIAN
|
||||
|
||||
#define GLIB_SYSDEF_POLLIN =1
|
||||
#define GLIB_SYSDEF_POLLOUT =4
|
||||
#define GLIB_SYSDEF_POLLPRI =2
|
||||
#define GLIB_SYSDEF_POLLHUP =16
|
||||
#define GLIB_SYSDEF_POLLERR =8
|
||||
#define GLIB_SYSDEF_POLLNVAL =32
|
||||
|
||||
/* No way to disable deprecation warnings for macros, so only emit deprecation
|
||||
* warnings on platforms where usage of this macro is broken */
|
||||
#if defined(__APPLE__) || defined(_MSC_VER) || defined(__CYGWIN__)
|
||||
#define G_MODULE_SUFFIX "so" GLIB_DEPRECATED_MACRO_IN_2_76
|
||||
#else
|
||||
#define G_MODULE_SUFFIX "so"
|
||||
#endif
|
||||
|
||||
typedef int GPid;
|
||||
#define G_PID_FORMAT "i"
|
||||
|
||||
#define GLIB_SYSDEF_AF_UNIX 1
|
||||
#define GLIB_SYSDEF_AF_INET 2
|
||||
#define GLIB_SYSDEF_AF_INET6 10
|
||||
|
||||
#define GLIB_SYSDEF_MSG_OOB 1
|
||||
#define GLIB_SYSDEF_MSG_PEEK 2
|
||||
#define GLIB_SYSDEF_MSG_DONTROUTE 4
|
||||
|
||||
#define G_DIR_SEPARATOR '/'
|
||||
#define G_DIR_SEPARATOR_S "/"
|
||||
#define G_SEARCHPATH_SEPARATOR ':'
|
||||
#define G_SEARCHPATH_SEPARATOR_S ":"
|
||||
|
||||
#undef G_HAVE_FREE_SIZED
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* __GLIBCONFIG_H__ */
|
||||
1
server/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js
generated
vendored
Normal file
1
server/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = __dirname;
|
||||
BIN
server/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.2
generated
vendored
Normal file
BIN
server/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.2
generated
vendored
Normal file
Binary file not shown.
42
server/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json
generated
vendored
Normal file
42
server/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "@img/sharp-libvips-linuxmusl-x64",
|
||||
"version": "1.2.3",
|
||||
"description": "Prebuilt libvips and dependencies for use with sharp on Linux (musl) x64",
|
||||
"author": "Lovell Fuller <npm@lovell.info>",
|
||||
"homepage": "https://sharp.pixelplumbing.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lovell/sharp-libvips.git",
|
||||
"directory": "npm/linuxmusl-x64"
|
||||
},
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"preferUnplugged": true,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"versions.json"
|
||||
],
|
||||
"type": "commonjs",
|
||||
"exports": {
|
||||
"./lib": "./lib/index.js",
|
||||
"./package": "./package.json",
|
||||
"./versions": "./versions.json"
|
||||
},
|
||||
"config": {
|
||||
"musl": ">=1.2.2"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
30
server/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json
generated
vendored
Normal file
30
server/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json
generated
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"aom": "3.13.1",
|
||||
"archive": "3.8.1",
|
||||
"cairo": "1.18.4",
|
||||
"cgif": "0.5.0",
|
||||
"exif": "0.6.25",
|
||||
"expat": "2.7.2",
|
||||
"ffi": "3.5.2",
|
||||
"fontconfig": "2.17.1",
|
||||
"freetype": "2.14.1",
|
||||
"fribidi": "1.0.16",
|
||||
"glib": "2.86.0",
|
||||
"harfbuzz": "11.5.0",
|
||||
"heif": "1.20.2",
|
||||
"highway": "1.3.0",
|
||||
"imagequant": "2.4.1",
|
||||
"lcms": "2.17",
|
||||
"mozjpeg": "4.1.5",
|
||||
"pango": "1.57.0",
|
||||
"pixman": "0.46.4",
|
||||
"png": "1.6.50",
|
||||
"proxy-libintl": "0.5",
|
||||
"rsvg": "2.61.1",
|
||||
"spng": "0.7.4",
|
||||
"tiff": "4.7.0",
|
||||
"vips": "8.17.2",
|
||||
"webp": "1.6.0",
|
||||
"xml2": "2.15.0",
|
||||
"zlib-ng": "2.2.5"
|
||||
}
|
||||
191
server/node_modules/@img/sharp-linuxmusl-x64/LICENSE
generated
vendored
Normal file
191
server/node_modules/@img/sharp-linuxmusl-x64/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, "control" means (i) the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets "[]" replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same "printed page" as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
18
server/node_modules/@img/sharp-linuxmusl-x64/README.md
generated
vendored
Normal file
18
server/node_modules/@img/sharp-linuxmusl-x64/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# `@img/sharp-linuxmusl-x64`
|
||||
|
||||
Prebuilt sharp for use with Linux (musl) x64.
|
||||
|
||||
## Licensing
|
||||
|
||||
Copyright 2013 Lovell Fuller and others.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
BIN
server/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node
generated
vendored
Normal file
BIN
server/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node
generated
vendored
Normal file
Binary file not shown.
46
server/node_modules/@img/sharp-linuxmusl-x64/package.json
generated
vendored
Normal file
46
server/node_modules/@img/sharp-linuxmusl-x64/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "@img/sharp-linuxmusl-x64",
|
||||
"version": "0.34.4",
|
||||
"description": "Prebuilt sharp for use with Linux (musl) x64",
|
||||
"author": "Lovell Fuller <npm@lovell.info>",
|
||||
"homepage": "https://sharp.pixelplumbing.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lovell/sharp.git",
|
||||
"directory": "npm/linuxmusl-x64"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"preferUnplugged": true,
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.3"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"exports": {
|
||||
"./sharp.node": "./lib/sharp-linuxmusl-x64.node",
|
||||
"./package": "./package.json"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"config": {
|
||||
"musl": ">=1.2.2"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
1
server/prod.pid
Normal file
1
server/prod.pid
Normal file
|
|
@ -0,0 +1 @@
|
|||
635326
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#FF6B6B;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#4ECDC4;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="300" height="300" fill="url(#grad1)"/>
|
||||
<text x="150" y="150" font-family="Arial, sans-serif" font-size="32" fill="white" text-anchor="middle" dominant-baseline="middle">🎵</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 520 B |
|
|
@ -1,10 +0,0 @@
|
|||
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="300" height="300" fill="url(#grad2)"/>
|
||||
<text x="150" y="150" font-family="Arial, sans-serif" font-size="32" fill="white" text-anchor="middle" dominant-baseline="middle">🎶</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 520 B |
|
|
@ -1,10 +0,0 @@
|
|||
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad3" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#f093fb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#f5576c;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="300" height="300" fill="url(#grad3)"/>
|
||||
<text x="150" y="150" font-family="Arial, sans-serif" font-size="32" fill="white" text-anchor="middle" dominant-baseline="middle">🎤</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 520 B |
|
|
@ -1,10 +0,0 @@
|
|||
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad4" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#4facfe;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00f2fe;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="300" height="300" fill="url(#grad4)"/>
|
||||
<text x="150" y="150" font-family="Arial, sans-serif" font-size="32" fill="white" text-anchor="middle" dominant-baseline="middle">🎸</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 520 B |
|
|
@ -1,10 +0,0 @@
|
|||
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad5" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#43e97b;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#38f9d7;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="300" height="300" fill="url(#grad5)"/>
|
||||
<text x="150" y="150" font-family="Arial, sans-serif" font-size="32" fill="white" text-anchor="middle" dominant-baseline="middle">🎹</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 520 B |
|
|
@ -8,20 +8,32 @@ function generateRandomPlaylistImage(): string {
|
|||
try {
|
||||
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
|
||||
|
||||
console.log(`[PLAYLIST COVER] Looking for images in directory: ${coversDir}`);
|
||||
|
||||
// Check if directory exists
|
||||
if (!fs.existsSync(coversDir)) {
|
||||
console.log('Playlist covers directory not found, creating it...');
|
||||
console.log('[PLAYLIST COVER] Directory not found, creating it...');
|
||||
fs.mkdirSync(coversDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Get all image files from the directory
|
||||
// Get all image files from the directory (Spotify only accepts JPEG for cover uploads)
|
||||
const files = fs.readdirSync(coversDir).filter(file => {
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
return ['.jpg', '.jpeg', '.png', '.webp', '.gif'].includes(ext);
|
||||
return ['.jpg', '.jpeg'].includes(ext); // Only JPEG files for Spotify upload
|
||||
});
|
||||
|
||||
console.log(`[PLAYLIST COVER] Found ${files.length} image files:`, files);
|
||||
|
||||
// Log file sizes for debugging
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(coversDir, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
const sizeKB = (stats.size / 1024).toFixed(2);
|
||||
console.log(`[PLAYLIST COVER] File: ${file}, Size: ${sizeKB} KB`);
|
||||
});
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No playlist cover images found in public/playlist-covers/');
|
||||
console.log('[PLAYLIST COVER] No playlist cover images found in public/playlist-covers/');
|
||||
return '/api/placeholder-playlist-cover';
|
||||
}
|
||||
|
||||
|
|
@ -29,11 +41,11 @@ function generateRandomPlaylistImage(): string {
|
|||
const randomFile = files[Math.floor(Math.random() * files.length)];
|
||||
const imageUrl = `/api/playlist-covers/${encodeURIComponent(randomFile)}`;
|
||||
|
||||
console.log(`Selected random playlist cover: ${imageUrl}`);
|
||||
console.log(`[PLAYLIST COVER] Selected random playlist cover: ${imageUrl} (from file: ${randomFile})`);
|
||||
return imageUrl;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating random playlist image:', error);
|
||||
console.error('[PLAYLIST COVER] Error generating random playlist image:', error);
|
||||
return '/api/placeholder-playlist-cover';
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +142,8 @@ export async function createSpotifyPlaylist(
|
|||
name: string,
|
||||
description: string
|
||||
): Promise<{ id: string; url: string; imageUrl?: string }> {
|
||||
console.log(`[PLAYLIST CREATION] Creating Spotify playlist: "${name}" for user: ${userId}`);
|
||||
|
||||
const resp = await axios.post(
|
||||
`https://api.spotify.com/v1/users/${encodeURIComponent(userId)}/playlists`,
|
||||
{ name, description, public: false },
|
||||
|
|
@ -137,9 +151,47 @@ export async function createSpotifyPlaylist(
|
|||
);
|
||||
const pl = resp.data as any;
|
||||
|
||||
console.log(`[PLAYLIST CREATION] Spotify playlist created successfully with ID: ${pl.id}`);
|
||||
|
||||
// Generate a random local playlist cover image
|
||||
const imageUrl = generateRandomPlaylistImage();
|
||||
|
||||
// Extract the filename from the imageUrl to use the SAME image for Spotify upload
|
||||
const selectedFilename = decodeURIComponent(imageUrl.replace('/api/playlist-covers/', ''));
|
||||
console.log(`[PLAYLIST COVER] Selected image for both website and Spotify: ${selectedFilename}`);
|
||||
|
||||
// Get the actual file path for uploading to Spotify (only JPEG files)
|
||||
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
|
||||
const files = fs.readdirSync(coversDir).filter(file => {
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
return ['.jpg', '.jpeg'].includes(ext); // Only JPEG files for Spotify upload
|
||||
});
|
||||
|
||||
if (files.length > 0) {
|
||||
// Find the specific file that was selected for the website
|
||||
const selectedFile = files.find(file => file === selectedFilename);
|
||||
|
||||
if (selectedFile) {
|
||||
const imagePath = path.join(coversDir, selectedFile);
|
||||
const stats = fs.statSync(imagePath);
|
||||
const fileSizeKB = (stats.size / 1024).toFixed(2);
|
||||
|
||||
console.log(`[PLAYLIST COVER] Using SAME image for Spotify upload: ${selectedFile} (${fileSizeKB} KB)`);
|
||||
|
||||
// Upload the SAME cover image to Spotify
|
||||
const uploadSuccess = await uploadPlaylistCover(accessToken, pl.id, imagePath);
|
||||
|
||||
if (uploadSuccess) {
|
||||
console.log(`[PLAYLIST COVER] ✅ Successfully uploaded SAME cover image to Spotify playlist ${pl.id}`);
|
||||
} else {
|
||||
console.log(`[PLAYLIST COVER] ❌ Failed to upload ${selectedFile} to Spotify, but website will still show it`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[PLAYLIST COVER] ⚠️ Could not find selected file ${selectedFilename} for Spotify upload`);
|
||||
console.log(`[PLAYLIST COVER] ✅ WEBSITE COVER: Random cover image will still be displayed on your website.`);
|
||||
}
|
||||
}
|
||||
|
||||
return { id: pl.id, url: pl.external_urls?.spotify, imageUrl };
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +208,155 @@ export async function addTracks(
|
|||
);
|
||||
}
|
||||
|
||||
// Upload a playlist cover image to Spotify
|
||||
export async function uploadPlaylistCover(
|
||||
accessToken: string,
|
||||
playlistId: string,
|
||||
imagePath: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
console.log(`[PLAYLIST COVER] Attempting to upload cover image for playlist ${playlistId} from path: ${imagePath}`);
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(imagePath)) {
|
||||
console.error(`[PLAYLIST COVER] Image file not found: ${imagePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the image file
|
||||
let imageBuffer = fs.readFileSync(imagePath);
|
||||
let fileSize = imageBuffer.length;
|
||||
|
||||
console.log(`[PLAYLIST COVER] Original image file size: ${fileSize} bytes (${(fileSize / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Check if we need to compress the image
|
||||
// We need to account for base64 encoding overhead (33% increase)
|
||||
// So we want the original file to be under ~190 KB to stay under 256 KB when base64 encoded
|
||||
const maxOriginalSize = 190 * 1024; // 190 KB
|
||||
|
||||
if (fileSize > maxOriginalSize) {
|
||||
console.log(`[PLAYLIST COVER] Image too large (${(fileSize / 1024).toFixed(2)} KB), attempting to compress...`);
|
||||
|
||||
// For now, let's skip files that are too large and try a different one
|
||||
console.error(`[PLAYLIST COVER] Image file too large for Spotify upload: ${fileSize} bytes (max ~190 KB to account for base64 overhead)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert to base64
|
||||
let base64Image = imageBuffer.toString('base64');
|
||||
const base64Size = base64Image.length;
|
||||
|
||||
console.log(`[PLAYLIST COVER] Base64 length: ${base64Size} characters (${(base64Size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Ensure we don't have any data URL prefix
|
||||
if (base64Image.startsWith('data:')) {
|
||||
base64Image = base64Image.split(',')[1] || base64Image;
|
||||
console.log(`[PLAYLIST COVER] Removed data URL prefix from base64`);
|
||||
}
|
||||
|
||||
// Final check - ensure base64 size is reasonable
|
||||
if (base64Size > 300 * 1024) { // 300 KB base64 limit
|
||||
console.error(`[PLAYLIST COVER] Base64 encoded image too large: ${base64Size} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine content type based on file extension
|
||||
const ext = path.extname(imagePath).toLowerCase();
|
||||
let contentType = 'image/jpeg'; // default
|
||||
switch (ext) {
|
||||
case '.png':
|
||||
contentType = 'image/png';
|
||||
break;
|
||||
case '.gif':
|
||||
contentType = 'image/gif';
|
||||
break;
|
||||
case '.webp':
|
||||
contentType = 'image/webp';
|
||||
break;
|
||||
case '.svg':
|
||||
contentType = 'image/svg+xml';
|
||||
break;
|
||||
case '.jpg':
|
||||
case '.jpeg':
|
||||
contentType = 'image/jpeg';
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`[PLAYLIST COVER] Using content type: ${contentType}`);
|
||||
|
||||
// Check token scopes first
|
||||
try {
|
||||
console.log(`[PLAYLIST COVER] Checking token scopes...`);
|
||||
const meResponse = await axios.get('https://api.spotify.com/v1/me', {
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||||
});
|
||||
console.log(`[PLAYLIST COVER] Token is valid for user: ${meResponse.data.display_name}`);
|
||||
|
||||
// Test if we can access the playlist
|
||||
const playlistResponse = await axios.get(`https://api.spotify.com/v1/playlists/${playlistId}`, {
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||||
});
|
||||
console.log(`[PLAYLIST COVER] Can access playlist: ${playlistResponse.data.name}`);
|
||||
} catch (error: any) {
|
||||
console.error(`[PLAYLIST COVER] Token validation failed:`, error.response?.data || error.message);
|
||||
}
|
||||
|
||||
// Upload to Spotify
|
||||
console.log(`[PLAYLIST COVER] Making request to Spotify API: https://api.spotify.com/v1/playlists/${playlistId}/images`);
|
||||
console.log(`[PLAYLIST COVER] Access token (first 20 chars): ${accessToken.substring(0, 20)}...`);
|
||||
|
||||
// Try different approaches for image upload
|
||||
let response;
|
||||
try {
|
||||
// Method 1: Standard PUT request (requires ugc-image-upload scope)
|
||||
response = await axios.put(
|
||||
`https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/images`,
|
||||
base64Image,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': contentType
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 401) {
|
||||
console.log(`[PLAYLIST COVER] Method 1 failed with 401, trying alternative approach...`);
|
||||
|
||||
// Method 2: Try with different content type
|
||||
try {
|
||||
response = await axios.put(
|
||||
`https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/images`,
|
||||
base64Image,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'image/jpeg' // Force JPEG content type
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error2: any) {
|
||||
console.log(`[PLAYLIST COVER] Method 2 also failed: ${error2.response?.status}`);
|
||||
throw error; // Re-throw original error
|
||||
}
|
||||
} else {
|
||||
throw error; // Re-throw if not 401
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[PLAYLIST COVER] Successfully uploaded cover image for playlist ${playlistId}, response status: ${response.status}`);
|
||||
console.log(`[PLAYLIST COVER] Response headers:`, response.headers);
|
||||
return true;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`[PLAYLIST COVER] Failed to upload cover image for playlist ${playlistId}:`);
|
||||
console.error(`[PLAYLIST COVER] Error status:`, error.response?.status);
|
||||
console.error(`[PLAYLIST COVER] Error data:`, error.response?.data);
|
||||
console.error(`[PLAYLIST COVER] Error message:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- New: Similar tracks without recommendations endpoint -----
|
||||
|
||||
async function getRelatedArtists(accessToken: string, artistId: string): Promise<string[]> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Router } from 'express';
|
||||
import { requireAuth, AuthedRequest } from '../middleware/auth.js';
|
||||
import { addTracks, createSpotifyPlaylist, ensureValidAccessToken, fetchRecommendations, vibeToTargets } from '../lib/spotify.js';
|
||||
import { addTracks, createSpotifyPlaylist, ensureValidAccessToken, fetchRecommendations, vibeToTargets, uploadPlaylistCover } from '../lib/spotify.js';
|
||||
import { db } from '../lib/db.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
|
@ -66,17 +66,26 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
|||
const creatorId = req.user!.uid;
|
||||
const { name, description, vibe, genres, includeKnown = true, partnerId, createForBoth = false, limit = 25 } = req.body || {};
|
||||
|
||||
console.log(`[MIXED PLAYLIST] Starting playlist creation process for creator: ${creatorId}, partner: ${partnerId}`);
|
||||
console.log(`[MIXED PLAYLIST] Request parameters:`, { name, description, vibe, genres, includeKnown, createForBoth, limit });
|
||||
|
||||
if (!partnerId) return res.status(400).json({ error: 'partnerId required' });
|
||||
|
||||
// Collect seeds from both users' top tracks
|
||||
console.log(`[MIXED PLAYLIST] Collecting seed tracks from creator (${creatorId}) and partner (${partnerId})`);
|
||||
const seedRows1 = db.db.prepare('SELECT track_json FROM top_tracks WHERE user_id=? AND time_range=? ORDER BY rank ASC LIMIT 20').all(creatorId, 'short_term');
|
||||
const seedRows2 = db.db.prepare('SELECT track_json FROM top_tracks WHERE user_id=? AND time_range=? ORDER BY rank ASC LIMIT 20').all(partnerId, 'short_term');
|
||||
const tracks1: Array<{ id?: string; uri?: string; artists?: Array<{ id?: string }> }> = seedRows1.map((r: any) => JSON.parse(r.track_json));
|
||||
const tracks2: Array<{ id?: string; uri?: string; artists?: Array<{ id?: string }> }> = seedRows2.map((r: any) => JSON.parse(r.track_json));
|
||||
|
||||
console.log(`[MIXED PLAYLIST] Found ${tracks1.length} tracks from creator, ${tracks2.length} tracks from partner`);
|
||||
|
||||
let seedTrackUris = [...tracks1, ...tracks2]
|
||||
.map((t) => t.uri || (t.id ? `spotify:track:${t.id}` : null))
|
||||
.filter((u): u is string => !!u);
|
||||
|
||||
console.log(`[MIXED PLAYLIST] Total seed track URIs: ${seedTrackUris.length}`);
|
||||
|
||||
// Fallback to artist seeds if track seeds are empty
|
||||
let seedArtistIds: string[] = [];
|
||||
if (seedTrackUris.length === 0) {
|
||||
|
|
@ -150,12 +159,17 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
|||
const playlistDesc = description || 'An AI-blended mix with fresh recommendations';
|
||||
|
||||
// Always create for the creator
|
||||
console.log(`[MIXED PLAYLIST] Creating Spotify playlist for creator: "${playlistName}"`);
|
||||
const creatorProfile = db.db.prepare('SELECT id FROM users WHERE id=?').get(creatorId) as { id: string };
|
||||
const { id: creatorPlaylistId, url: creatorUrl, imageUrl: creatorImageUrl } = await createSpotifyPlaylist(creatorToken, creatorProfile.id, playlistName, playlistDesc);
|
||||
|
||||
console.log(`[MIXED PLAYLIST] Adding ${finalUris.length} tracks to creator playlist ${creatorPlaylistId}`);
|
||||
await addTracks(creatorToken, creatorPlaylistId, finalUris);
|
||||
console.log(`[MIXED PLAYLIST] Successfully added tracks to creator playlist`);
|
||||
|
||||
let partnerResult: { id?: string; url?: string; imageUrl?: string; error?: string } = {};
|
||||
if (createForBoth) {
|
||||
console.log(`[MIXED PLAYLIST] Creating playlist for partner as well: ${partnerId}`);
|
||||
try {
|
||||
const partnerToken = await ensureValidAccessToken(partnerId);
|
||||
const partnerProfile = db.db.prepare('SELECT id FROM users WHERE id=?').get(partnerId) as { id: string };
|
||||
|
|
@ -164,15 +178,17 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
|||
throw new Error('Partner user not found');
|
||||
}
|
||||
|
||||
console.log(`[MIXED PLAYLIST] Creating Spotify playlist for partner: "${playlistName}"`);
|
||||
const partnerPlaylist = await createSpotifyPlaylist(partnerToken, partnerProfile.id, playlistName, playlistDesc);
|
||||
partnerResult = partnerPlaylist;
|
||||
await addTracks(partnerToken, partnerResult.id!, finalUris);
|
||||
|
||||
console.log(`Successfully created partner playlist: ${partnerResult.id}`);
|
||||
console.log(`[MIXED PLAYLIST] Adding ${finalUris.length} tracks to partner playlist ${partnerResult.id}`);
|
||||
await addTracks(partnerToken, partnerResult.id!, finalUris);
|
||||
console.log(`[MIXED PLAYLIST] Successfully created partner playlist: ${partnerResult.id}`);
|
||||
} catch (e) {
|
||||
// Partner creation may fail (revoked consent, user not found, etc.); still return creator playlist
|
||||
const errorMessage = (e as any)?.response?.data?.error?.message || (e as any)?.message || 'Unknown error';
|
||||
console.error('Partner playlist creation failed:', errorMessage);
|
||||
console.error('[MIXED PLAYLIST] Partner playlist creation failed:', errorMessage);
|
||||
partnerResult.error = errorMessage;
|
||||
}
|
||||
}
|
||||
|
|
@ -181,10 +197,13 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
|||
const playlistId = uuidv4();
|
||||
const now = Date.now();
|
||||
|
||||
console.log('Saving playlist to database:', {
|
||||
console.log('[MIXED PLAYLIST] Saving playlist to database:', {
|
||||
id: playlistId,
|
||||
name: playlistName,
|
||||
trackCount: finalUris.length,
|
||||
creatorPlaylistId,
|
||||
partnerPlaylistId: partnerResult.id,
|
||||
imageUrl: creatorImageUrl,
|
||||
trackUris: finalUris.slice(0, 3) // Log first 3 URIs for debugging
|
||||
});
|
||||
|
||||
|
|
@ -214,7 +233,7 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
|||
now
|
||||
);
|
||||
|
||||
res.json({
|
||||
const response = {
|
||||
id: playlistId,
|
||||
name: playlistName,
|
||||
description: playlistDesc,
|
||||
|
|
@ -226,10 +245,22 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
|||
partnerError: partnerResult.error || null,
|
||||
},
|
||||
added: finalUris.length,
|
||||
};
|
||||
|
||||
console.log('[MIXED PLAYLIST] Playlist creation completed successfully:', {
|
||||
id: playlistId,
|
||||
name: playlistName,
|
||||
creatorPlaylistId,
|
||||
partnerPlaylistId: partnerResult.id,
|
||||
trackCount: finalUris.length,
|
||||
imageUrl: creatorImageUrl
|
||||
});
|
||||
|
||||
res.json(response);
|
||||
} catch (err: any) {
|
||||
const detail = err?.response?.data || err?.message || 'Unknown error';
|
||||
console.error('Mixed playlist error:', detail);
|
||||
console.error('[MIXED PLAYLIST] Error during playlist creation:', detail);
|
||||
console.error('[MIXED PLAYLIST] Full error object:', err);
|
||||
res.status(500).json({ error: 'Failed to create mixed playlist', detail });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
BIN
spotify.db
BIN
spotify.db
Binary file not shown.
BIN
spotify.db-shm
BIN
spotify.db-shm
Binary file not shown.
BIN
spotify.db-wal
BIN
spotify.db-wal
Binary file not shown.
|
|
@ -8,7 +8,7 @@ function getApiBase(): string {
|
|||
const isHttpsPage = typeof window !== 'undefined' && window.location.protocol === 'https:';
|
||||
const u = new URL(base, window.location.origin);
|
||||
if (isHttpsPage && u.protocol === 'http:' && u.hostname === '159.195.9.107' && (u.port === '8081' || u.port === '')) {
|
||||
return 'https://159.195.9.107:8082';
|
||||
return 'https://159.195.9.107:3443';
|
||||
}
|
||||
} catch {
|
||||
// ignore URL parse issues and fall through to returned base
|
||||
|
|
@ -24,7 +24,7 @@ function getApiBase(): string {
|
|||
// For production/self-hosted: choose port based on protocol to avoid mixed content
|
||||
if (window.location.hostname === '159.195.9.107') {
|
||||
const isHttps = window.location.protocol === 'https:';
|
||||
return isHttps ? 'https://159.195.9.107:8082' : 'http://159.195.9.107:8081';
|
||||
return isHttps ? 'https://159.195.9.107:3443' : 'http://159.195.9.107:8081';
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const getSpotifyAuthUrl = (): string => {
|
|||
'user-read-playback-state',
|
||||
'user-modify-playback-state',
|
||||
'user-read-currently-playing',
|
||||
'ugc-image-upload', // Required for uploading playlist cover images
|
||||
].join(' ');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user