fix bug about sending playlist pictures

This commit is contained in:
pschneid 2025-10-16 15:51:31 +02:00
parent c678b60b69
commit af5b20d3e6
34 changed files with 1952 additions and 477 deletions

File diff suppressed because one or more lines are too long

2
dist/index.html vendored
View File

@ -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>

Binary file not shown.

2
preview-3443.out Normal file
View 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)

BIN
prod.out Normal file

Binary file not shown.

1
prod.pid Normal file
View File

@ -0,0 +1 @@
631996

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
592323
619472

View File

@ -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`, {

View File

@ -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 });
}
});

View File

@ -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",

View 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

View 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__ */

View File

@ -0,0 +1 @@
module.exports = __dirname;

Binary file not shown.

View 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"
]
}

View 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
View 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
View 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.

Binary file not shown.

View 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
View File

@ -0,0 +1 @@
635326

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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[]> {

View File

@ -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,16 +66,25 @@ 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[] = [];
@ -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 });
}
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -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({