import express from 'express'; import { Playlist } from '../models/Playlist.js'; import { Request, Response } from 'express'; const router = express.Router(); // Get all playlists router.get('/', async (req: Request, res: Response) => { try { const playlists = await Playlist.find({}); res.json(playlists); } catch (error) { console.error('Error fetching playlists:', error); res.status(500).json({ message: 'Error fetching playlists', error }); } }); // Get playlist structure only (without track data) for faster loading router.get('/structure', async (req: Request, res: Response) => { try { const playlists = await Playlist.find({}); // Keep track counts but remove full track data to reduce payload size const structureOnly = playlists.map(playlist => { const cleanPlaylist = playlist.toObject() as any; // Replace tracks array with track count for the main playlist if (cleanPlaylist.tracks) { cleanPlaylist.trackCount = cleanPlaylist.tracks.length; delete cleanPlaylist.tracks; } else { cleanPlaylist.trackCount = 0; } // Recursively process children const cleanChildren = (children: any[]): any[] => { return children.map(child => { const cleanChild = { ...child } as any; // Replace tracks array with track count if (cleanChild.tracks) { cleanChild.trackCount = cleanChild.tracks.length; delete cleanChild.tracks; } else { cleanChild.trackCount = 0; } if (cleanChild.children && cleanChild.children.length > 0) { cleanChild.children = cleanChildren(cleanChild.children); } return cleanChild; }); }; if (cleanPlaylist.children && cleanPlaylist.children.length > 0) { cleanPlaylist.children = cleanChildren(cleanPlaylist.children); } return cleanPlaylist; }); res.json(structureOnly); } catch (error) { console.error('Error fetching playlist structure:', error); res.status(500).json({ message: 'Error fetching playlist structure', error }); } }); // Save playlists in batch (replacing all existing ones) router.post('/batch', async (req, res) => { try { // Replace all playlists atomically await Playlist.deleteMany({}); const payload = Array.isArray(req.body) ? req.body : []; const playlists = await Playlist.insertMany(payload, { ordered: false }); res.json(playlists); } catch (error) { console.error('Error saving playlists:', error); res.status(500).json({ error: 'Failed to save playlists' }); } }); // Reorder tracks inside a playlist (by name). Stores order separately in `order` to avoid interfering with original `tracks`. router.post('/reorder', async (req: Request, res: Response) => { try { const { playlistName, orderedIds } = req.body as { playlistName: string; orderedIds: string[] }; if (!playlistName || !Array.isArray(orderedIds)) { return res.status(400).json({ message: 'playlistName and orderedIds are required' }); } const playlists = await Playlist.find({}); let updated = false; const reorderNode = (node: any): any => { if (!node) return node; if (node.type === 'playlist' && node.name === playlistName) { // Store order separately; do not mutate tracks const unique = Array.from(new Set(orderedIds)); node.order = unique; updated = true; return node; } if (Array.isArray(node.children)) { node.children = node.children.map(reorderNode); } return node; }; for (const p of playlists) { reorderNode(p as any); // Force Mongoose to see deep changes under Mixed children p.markModified('children'); p.markModified('tracks'); await p.save(); } if (!updated) { return res.status(404).json({ message: `Playlist "${playlistName}" not found` }); } res.json({ message: 'Playlist order updated', playlistName, newLength: orderedIds.length }); } catch (error) { console.error('Error reordering playlist:', error); res.status(500).json({ error: 'Failed to reorder playlist' }); } }); // Move a single track before another (stable, full-playlist context) router.post('/reorder-move', async (req: Request, res: Response) => { try { const { playlistName, fromId, toId } = req.body as { playlistName: string; fromId: string; toId?: string }; if (!playlistName || !fromId) { return res.status(400).json({ message: 'playlistName and fromId are required' }); } const playlists = await Playlist.find({}); let updated = false; const applyMove = (node: any): any => { if (!node) return node; if (node.type === 'playlist' && node.name === playlistName) { const base: string[] = Array.isArray(node.tracks) ? node.tracks : []; const order: string[] = Array.isArray(node.order) ? node.order : []; // Build effective order const baseSet = new Set(base); const effective = [ ...order.filter((id: string) => baseSet.has(id)), ...base.filter((id: string) => !order.includes(id)) ]; // Remove fromId if present const without = effective.filter(id => id !== fromId); let insertIndex = typeof toId === 'string' ? without.indexOf(toId) : without.length; if (insertIndex < 0) insertIndex = without.length; without.splice(insertIndex, 0, fromId); console.log('[REORDER_MOVE] playlist:', playlistName, 'from:', fromId, 'to:', toId, 'baseLen:', base.length, 'orderLenBefore:', order.length, 'orderLenAfter:', without.length); console.log('[REORDER_MOVE] sample order:', without.slice(0, 5)); node.order = without; // store full order overlay updated = true; return node; } if (Array.isArray(node.children)) { node.children = node.children.map(applyMove); } return node; }; for (const p of playlists) { applyMove(p as any); p.markModified('children'); p.markModified('order'); await p.save(); } if (!updated) { return res.status(404).json({ message: `Playlist "${playlistName}" not found` }); } res.json({ message: 'Track moved', playlistName }); } catch (error) { console.error('Error moving track in playlist:', error); res.status(500).json({ error: 'Failed to move track' }); } }); // Move multiple tracks before another (en bloc, preserves relative order) router.post('/reorder-move-many', async (req: Request, res: Response) => { try { const { playlistName, fromIds, toId } = req.body as { playlistName: string; fromIds: string[]; toId?: string }; if (!playlistName || !Array.isArray(fromIds) || fromIds.length === 0) { return res.status(400).json({ message: 'playlistName and fromIds are required' }); } const playlists = await Playlist.find({}); let updated = false; const applyMoveMany = (node: any): any => { if (!node) return node; if (node.type === 'playlist' && node.name === playlistName) { const base: string[] = Array.isArray(node.tracks) ? node.tracks : []; const order: string[] = Array.isArray(node.order) ? node.order : []; const baseSet = new Set(base); const effective: string[] = [ ...order.filter((id: string) => baseSet.has(id)), ...base.filter((id: string) => !order.includes(id)) ]; const moveSet = new Set(fromIds); const movingOrdered = effective.filter(id => moveSet.has(id)); if (movingOrdered.length === 0) { return node; } // Remove all moving ids const without = effective.filter(id => !moveSet.has(id)); let insertIndex = typeof toId === 'string' ? without.indexOf(toId) : without.length; if (insertIndex < 0) insertIndex = without.length; // Insert block preserving relative order without.splice(insertIndex, 0, ...movingOrdered); console.log('[REORDER_MOVE_MANY] playlist:', playlistName, 'count:', movingOrdered.length, 'to:', toId, 'baseLen:', base.length, 'orderLenBefore:', order.length, 'orderLenAfter:', without.length); console.log('[REORDER_MOVE_MANY] sample order:', without.slice(0, 7)); node.order = without; updated = true; return node; } if (Array.isArray(node.children)) { node.children = node.children.map(applyMoveMany); } return node; }; for (const p of playlists) { applyMoveMany(p as any); p.markModified('children'); p.markModified('order'); await p.save(); } if (!updated) { return res.status(404).json({ message: `Playlist "${playlistName}" not found` }); } res.json({ message: 'Tracks moved', playlistName }); } catch (error) { console.error('Error moving tracks in playlist:', error); res.status(500).json({ message: 'Error moving tracks in playlist', error }); } }); export const playlistsRouter = router;