250 lines
8.5 KiB
TypeScript
250 lines
8.5 KiB
TypeScript
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);
|
|
|
|
// Updated playlist order overlay
|
|
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);
|
|
|
|
// Updated playlist order overlay for batch move
|
|
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;
|