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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>💕 Our Musical Journey</title>
|
<title>💕 Our Musical Journey</title>
|
||||||
<meta name="description" content="A private musical journey for two hearts connected through music" />
|
<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">
|
<link rel="stylesheet" crossorigin href="/assets/index-HYQ6lVkA.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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() {
|
function generateRandomPlaylistImage() {
|
||||||
try {
|
try {
|
||||||
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
|
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
|
||||||
|
console.log(`[PLAYLIST COVER] Looking for images in directory: ${coversDir}`);
|
||||||
// Check if directory exists
|
// Check if directory exists
|
||||||
if (!fs.existsSync(coversDir)) {
|
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 });
|
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 files = fs.readdirSync(coversDir).filter(file => {
|
||||||
const ext = path.extname(file).toLowerCase();
|
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) {
|
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';
|
return '/api/placeholder-playlist-cover';
|
||||||
}
|
}
|
||||||
// Select a random image
|
// Select a random image
|
||||||
const randomFile = files[Math.floor(Math.random() * files.length)];
|
const randomFile = files[Math.floor(Math.random() * files.length)];
|
||||||
const imageUrl = `/api/playlist-covers/${encodeURIComponent(randomFile)}`;
|
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;
|
return imageUrl;
|
||||||
}
|
}
|
||||||
catch (error) {
|
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';
|
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);
|
return fetchSimilarTrackUris(accessToken, seeds.seed_tracks, seeds.seed_artists, seeds.seed_genres, targets, limit);
|
||||||
}
|
}
|
||||||
export async function createSpotifyPlaylist(accessToken, userId, name, description) {
|
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 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;
|
const pl = resp.data;
|
||||||
|
console.log(`[PLAYLIST CREATION] Spotify playlist created successfully with ID: ${pl.id}`);
|
||||||
// Generate a random local playlist cover image
|
// Generate a random local playlist cover image
|
||||||
const imageUrl = generateRandomPlaylistImage();
|
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 };
|
return { id: pl.id, url: pl.external_urls?.spotify, imageUrl };
|
||||||
}
|
}
|
||||||
export async function addTracks(accessToken, playlistId, trackUris) {
|
export async function addTracks(accessToken, playlistId, trackUris) {
|
||||||
|
|
@ -105,6 +147,128 @@ export async function addTracks(accessToken, playlistId, trackUris) {
|
||||||
return;
|
return;
|
||||||
await axios.post(`https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/tracks`, { uris: trackUris }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } });
|
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 -----
|
// ----- New: Similar tracks without recommendations endpoint -----
|
||||||
async function getRelatedArtists(accessToken, artistId) {
|
async function getRelatedArtists(accessToken, artistId) {
|
||||||
const resp = await axios.get(`https://api.spotify.com/v1/artists/${encodeURIComponent(artistId)}/related-artists`, {
|
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 {
|
try {
|
||||||
const creatorId = req.user.uid;
|
const creatorId = req.user.uid;
|
||||||
const { name, description, vibe, genres, includeKnown = true, partnerId, createForBoth = false, limit = 25 } = req.body || {};
|
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)
|
if (!partnerId)
|
||||||
return res.status(400).json({ error: 'partnerId required' });
|
return res.status(400).json({ error: 'partnerId required' });
|
||||||
// Collect seeds from both users' top tracks
|
// 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 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 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 tracks1 = seedRows1.map((r) => JSON.parse(r.track_json));
|
||||||
const tracks2 = seedRows2.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]
|
let seedTrackUris = [...tracks1, ...tracks2]
|
||||||
.map((t) => t.uri || (t.id ? `spotify:track:${t.id}` : null))
|
.map((t) => t.uri || (t.id ? `spotify:track:${t.id}` : null))
|
||||||
.filter((u) => !!u);
|
.filter((u) => !!u);
|
||||||
|
console.log(`[MIXED PLAYLIST] Total seed track URIs: ${seedTrackUris.length}`);
|
||||||
// Fallback to artist seeds if track seeds are empty
|
// Fallback to artist seeds if track seeds are empty
|
||||||
let seedArtistIds = [];
|
let seedArtistIds = [];
|
||||||
if (seedTrackUris.length === 0) {
|
if (seedTrackUris.length === 0) {
|
||||||
|
|
@ -123,36 +128,45 @@ playlistsRouter.post('/mixed', requireAuth, async (req, res) => {
|
||||||
const playlistName = name || generateUniquePlaylistName();
|
const playlistName = name || generateUniquePlaylistName();
|
||||||
const playlistDesc = description || 'An AI-blended mix with fresh recommendations';
|
const playlistDesc = description || 'An AI-blended mix with fresh recommendations';
|
||||||
// Always create for the creator
|
// 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 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);
|
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);
|
await addTracks(creatorToken, creatorPlaylistId, finalUris);
|
||||||
|
console.log(`[MIXED PLAYLIST] Successfully added tracks to creator playlist`);
|
||||||
let partnerResult = {};
|
let partnerResult = {};
|
||||||
if (createForBoth) {
|
if (createForBoth) {
|
||||||
|
console.log(`[MIXED PLAYLIST] Creating playlist for partner as well: ${partnerId}`);
|
||||||
try {
|
try {
|
||||||
const partnerToken = await ensureValidAccessToken(partnerId);
|
const partnerToken = await ensureValidAccessToken(partnerId);
|
||||||
const partnerProfile = db.db.prepare('SELECT id FROM users WHERE id=?').get(partnerId);
|
const partnerProfile = db.db.prepare('SELECT id FROM users WHERE id=?').get(partnerId);
|
||||||
if (!partnerProfile) {
|
if (!partnerProfile) {
|
||||||
throw new Error('Partner user not found');
|
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);
|
const partnerPlaylist = await createSpotifyPlaylist(partnerToken, partnerProfile.id, playlistName, playlistDesc);
|
||||||
partnerResult = partnerPlaylist;
|
partnerResult = partnerPlaylist;
|
||||||
|
console.log(`[MIXED PLAYLIST] Adding ${finalUris.length} tracks to partner playlist ${partnerResult.id}`);
|
||||||
await addTracks(partnerToken, partnerResult.id, finalUris);
|
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) {
|
catch (e) {
|
||||||
// Partner creation may fail (revoked consent, user not found, etc.); still return creator playlist
|
// 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';
|
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;
|
partnerResult.error = errorMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Save playlist to database
|
// Save playlist to database
|
||||||
const playlistId = uuidv4();
|
const playlistId = uuidv4();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
console.log('Saving playlist to database:', {
|
console.log('[MIXED PLAYLIST] Saving playlist to database:', {
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
name: playlistName,
|
name: playlistName,
|
||||||
trackCount: finalUris.length,
|
trackCount: finalUris.length,
|
||||||
|
creatorPlaylistId,
|
||||||
|
partnerPlaylistId: partnerResult.id,
|
||||||
|
imageUrl: creatorImageUrl,
|
||||||
trackUris: finalUris.slice(0, 3) // Log first 3 URIs for debugging
|
trackUris: finalUris.slice(0, 3) // Log first 3 URIs for debugging
|
||||||
});
|
});
|
||||||
db.db.prepare(`
|
db.db.prepare(`
|
||||||
|
|
@ -163,7 +177,7 @@ playlistsRouter.post('/mixed', requireAuth, async (req, res) => {
|
||||||
created_at, updated_at
|
created_at, updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) 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);
|
`).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,
|
id: playlistId,
|
||||||
name: playlistName,
|
name: playlistName,
|
||||||
description: playlistDesc,
|
description: playlistDesc,
|
||||||
|
|
@ -175,11 +189,21 @@ playlistsRouter.post('/mixed', requireAuth, async (req, res) => {
|
||||||
partnerError: partnerResult.error || null,
|
partnerError: partnerResult.error || null,
|
||||||
},
|
},
|
||||||
added: finalUris.length,
|
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) {
|
catch (err) {
|
||||||
const detail = err?.response?.data || err?.message || 'Unknown error';
|
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 });
|
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"
|
"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": {
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
"version": "0.34.4",
|
"version": "0.34.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz",
|
"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"
|
"@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": {
|
"node_modules/@types/better-sqlite3": {
|
||||||
"version": "7.6.13",
|
"version": "7.6.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
"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 {
|
try {
|
||||||
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
|
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
|
||||||
|
|
||||||
|
console.log(`[PLAYLIST COVER] Looking for images in directory: ${coversDir}`);
|
||||||
|
|
||||||
// Check if directory exists
|
// Check if directory exists
|
||||||
if (!fs.existsSync(coversDir)) {
|
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 });
|
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 files = fs.readdirSync(coversDir).filter(file => {
|
||||||
const ext = path.extname(file).toLowerCase();
|
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) {
|
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';
|
return '/api/placeholder-playlist-cover';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,11 +41,11 @@ function generateRandomPlaylistImage(): string {
|
||||||
const randomFile = files[Math.floor(Math.random() * files.length)];
|
const randomFile = files[Math.floor(Math.random() * files.length)];
|
||||||
const imageUrl = `/api/playlist-covers/${encodeURIComponent(randomFile)}`;
|
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;
|
return imageUrl;
|
||||||
|
|
||||||
} catch (error) {
|
} 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';
|
return '/api/placeholder-playlist-cover';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +142,8 @@ export async function createSpotifyPlaylist(
|
||||||
name: string,
|
name: string,
|
||||||
description: string
|
description: string
|
||||||
): Promise<{ id: string; url: string; imageUrl?: string }> {
|
): Promise<{ id: string; url: string; imageUrl?: string }> {
|
||||||
|
console.log(`[PLAYLIST CREATION] Creating Spotify playlist: "${name}" for user: ${userId}`);
|
||||||
|
|
||||||
const resp = await axios.post(
|
const resp = await axios.post(
|
||||||
`https://api.spotify.com/v1/users/${encodeURIComponent(userId)}/playlists`,
|
`https://api.spotify.com/v1/users/${encodeURIComponent(userId)}/playlists`,
|
||||||
{ name, description, public: false },
|
{ name, description, public: false },
|
||||||
|
|
@ -137,9 +151,47 @@ export async function createSpotifyPlaylist(
|
||||||
);
|
);
|
||||||
const pl = resp.data as any;
|
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
|
// Generate a random local playlist cover image
|
||||||
const imageUrl = generateRandomPlaylistImage();
|
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 };
|
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 -----
|
// ----- New: Similar tracks without recommendations endpoint -----
|
||||||
|
|
||||||
async function getRelatedArtists(accessToken: string, artistId: string): Promise<string[]> {
|
async function getRelatedArtists(accessToken: string, artistId: string): Promise<string[]> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { requireAuth, AuthedRequest } from '../middleware/auth.js';
|
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 { db } from '../lib/db.js';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
|
@ -66,16 +66,25 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
||||||
const creatorId = req.user!.uid;
|
const creatorId = req.user!.uid;
|
||||||
const { name, description, vibe, genres, includeKnown = true, partnerId, createForBoth = false, limit = 25 } = req.body || {};
|
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' });
|
if (!partnerId) return res.status(400).json({ error: 'partnerId required' });
|
||||||
|
|
||||||
// Collect seeds from both users' top tracks
|
// 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 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 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 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));
|
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]
|
let seedTrackUris = [...tracks1, ...tracks2]
|
||||||
.map((t) => t.uri || (t.id ? `spotify:track:${t.id}` : null))
|
.map((t) => t.uri || (t.id ? `spotify:track:${t.id}` : null))
|
||||||
.filter((u): u is string => !!u);
|
.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
|
// Fallback to artist seeds if track seeds are empty
|
||||||
let seedArtistIds: string[] = [];
|
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';
|
const playlistDesc = description || 'An AI-blended mix with fresh recommendations';
|
||||||
|
|
||||||
// Always create for the creator
|
// 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 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);
|
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);
|
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 } = {};
|
let partnerResult: { id?: string; url?: string; imageUrl?: string; error?: string } = {};
|
||||||
if (createForBoth) {
|
if (createForBoth) {
|
||||||
|
console.log(`[MIXED PLAYLIST] Creating playlist for partner as well: ${partnerId}`);
|
||||||
try {
|
try {
|
||||||
const partnerToken = await ensureValidAccessToken(partnerId);
|
const partnerToken = await ensureValidAccessToken(partnerId);
|
||||||
const partnerProfile = db.db.prepare('SELECT id FROM users WHERE id=?').get(partnerId) as { id: string };
|
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');
|
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);
|
const partnerPlaylist = await createSpotifyPlaylist(partnerToken, partnerProfile.id, playlistName, playlistDesc);
|
||||||
partnerResult = partnerPlaylist;
|
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) {
|
} catch (e) {
|
||||||
// Partner creation may fail (revoked consent, user not found, etc.); still return creator playlist
|
// 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';
|
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;
|
partnerResult.error = errorMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -181,10 +197,13 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
||||||
const playlistId = uuidv4();
|
const playlistId = uuidv4();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
console.log('Saving playlist to database:', {
|
console.log('[MIXED PLAYLIST] Saving playlist to database:', {
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
name: playlistName,
|
name: playlistName,
|
||||||
trackCount: finalUris.length,
|
trackCount: finalUris.length,
|
||||||
|
creatorPlaylistId,
|
||||||
|
partnerPlaylistId: partnerResult.id,
|
||||||
|
imageUrl: creatorImageUrl,
|
||||||
trackUris: finalUris.slice(0, 3) // Log first 3 URIs for debugging
|
trackUris: finalUris.slice(0, 3) // Log first 3 URIs for debugging
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -214,7 +233,7 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
||||||
now
|
now
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
const response = {
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
name: playlistName,
|
name: playlistName,
|
||||||
description: playlistDesc,
|
description: playlistDesc,
|
||||||
|
|
@ -226,10 +245,22 @@ playlistsRouter.post('/mixed', requireAuth, async (req: AuthedRequest, res) => {
|
||||||
partnerError: partnerResult.error || null,
|
partnerError: partnerResult.error || null,
|
||||||
},
|
},
|
||||||
added: finalUris.length,
|
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) {
|
} catch (err: any) {
|
||||||
const detail = err?.response?.data || err?.message || 'Unknown error';
|
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 });
|
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 isHttpsPage = typeof window !== 'undefined' && window.location.protocol === 'https:';
|
||||||
const u = new URL(base, window.location.origin);
|
const u = new URL(base, window.location.origin);
|
||||||
if (isHttpsPage && u.protocol === 'http:' && u.hostname === '159.195.9.107' && (u.port === '8081' || u.port === '')) {
|
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 {
|
} catch {
|
||||||
// ignore URL parse issues and fall through to returned base
|
// 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
|
// For production/self-hosted: choose port based on protocol to avoid mixed content
|
||||||
if (window.location.hostname === '159.195.9.107') {
|
if (window.location.hostname === '159.195.9.107') {
|
||||||
const isHttps = window.location.protocol === 'https:';
|
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
|
// Default fallback
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export const getSpotifyAuthUrl = (): string => {
|
||||||
'user-read-playback-state',
|
'user-read-playback-state',
|
||||||
'user-modify-playback-state',
|
'user-modify-playback-state',
|
||||||
'user-read-currently-playing',
|
'user-read-currently-playing',
|
||||||
|
'ugc-image-upload', // Required for uploading playlist cover images
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user