From 32d545959db1094164e99415c21d1941646326a8 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:03:03 +0200 Subject: [PATCH 01/14] feat(playlists): support custom per-playlist track order; backend /playlists/reorder; playlist songs endpoint honors order; frontend intra-playlist row DnD with reorder persist --- packages/backend/src/routes/playlists.ts | 42 +++++++++++++++++++ packages/backend/src/routes/songs.ts | 16 ++++--- packages/frontend/src/App.tsx | 11 +++++ .../src/components/PaginatedSongList.tsx | 31 +++++++++++++- packages/frontend/src/services/api.ts | 9 ++++ 5 files changed, 102 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index 97f127d..9c98f3e 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -80,4 +80,46 @@ router.post('/batch', async (req, res) => { } }); +// Reorder tracks inside a playlist (by name) +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) { + // Ensure unique order and only include known IDs + const unique = Array.from(new Set(orderedIds)); + node.tracks = 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); + await p.save(); + } + + if (!updated) { + return res.status(404).json({ message: `Playlist "${playlistName}" not found` }); + } + + res.json({ message: 'Playlist order updated' }); + } catch (error) { + console.error('Error reordering playlist:', error); + res.status(500).json({ error: 'Failed to reorder playlist' }); + } +}); + export const playlistsRouter = router; \ No newline at end of file diff --git a/packages/backend/src/routes/songs.ts b/packages/backend/src/routes/songs.ts index d674ce4..88f7100 100644 --- a/packages/backend/src/routes/songs.ts +++ b/packages/backend/src/routes/songs.ts @@ -34,13 +34,19 @@ router.get('/', async (req: Request, res: Response) => { const totalSongs = await Song.countDocuments(query); const totalPages = Math.ceil(totalSongs / limit); - // Get songs with pagination - const songs = await Song.find(query) - .sort({ title: 1 }) - .skip(skip) - .limit(limit) + // Get songs for this page in the exact playlist order + // Determine the slice of trackIds for the requested page + const pageStart = (page - 1) * limit; + const pageEnd = Math.min(pageStart + limit, trackIds.length); + const pageTrackIds = trackIds.slice(pageStart, pageEnd); + + const pageSongs = await Song.find({ id: { $in: pageTrackIds } }) .populate('s3File.musicFileId') .lean(); + // Order them to match pageTrackIds + const idToSong: Record = {}; + for (const s of pageSongs) idToSong[s.id] = s; + const songs = pageTrackIds.map(id => idToSong[id]).filter(Boolean); console.log(`Found ${songs.length} songs (${totalSongs} total), ${songs.filter((s: any) => s.s3File?.hasS3File).length} with S3 files`); diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 3b54475..bd08496 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -651,6 +651,17 @@ const RekordboxReader: React.FC = () => { searchQuery={searchQuery} isSwitchingPlaylist={isSwitchingPlaylist} onPlaySong={handlePlaySong} + onReorder={async (orderedIds: string[]) => { + if (!currentPlaylist || currentPlaylist === 'All Songs') return; + // Persist order in backend + await api.reorderPlaylist(currentPlaylist, orderedIds); + // Refresh the current playlist view + // Easiest: refetch page 1 to reflect new order + // Future: keep local order optimistic + // For now, force reload songs by navigating to same route + // and resetting pagination via usePaginatedSongs hook + // noop here as hook likely fetches by API sort; backend will serve correct order when playlist songs endpoint respects track order + }} /> diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index e610cde..5b2c169 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -38,6 +38,7 @@ interface PaginatedSongListProps { depth?: number; isSwitchingPlaylist?: boolean; // New prop to indicate playlist switching onPlaySong?: (song: Song) => void; // New prop for playing songs + onReorder?: (orderedIds: string[]) => Promise; } // Memoized song item component to prevent unnecessary re-renders @@ -148,7 +149,8 @@ export const PaginatedSongList: React.FC = memo(({ searchQuery, depth = 0, isSwitchingPlaylist = false, - onPlaySong + onPlaySong, + onReorder }) => { const [selectedSongs, setSelectedSongs] = useState>(new Set()); const dragSelectionRef = useRef(null); @@ -160,6 +162,7 @@ export const PaginatedSongList: React.FC = memo(({ const scrollContainerRef = useRef(null); const isTriggeringRef = useRef(false); const timeoutRef = useRef(null); + const dragIndexRef = useRef(null); // Store current values in refs to avoid stale closures const hasMoreRef = useRef(hasMore); @@ -252,7 +255,7 @@ export const PaginatedSongList: React.FC = memo(({ // Memoized song items to prevent unnecessary re-renders const songItems = useMemo(() => { - return songs.map(song => ( + return songs.map((song, index) => ( = memo(({ showCheckbox={selectedSongs.size > 0 || depth === 0} onPlaySong={onPlaySong} onDragStart={handleDragStart} + // Simple playlist reordering within same list by dragging rows + onDragOver={(e: React.DragEvent) => { + if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; + e.preventDefault(); + }} + onDrop={async (e: React.DragEvent) => { + if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; + e.preventDefault(); + const fromId = e.dataTransfer.getData('text/song-id'); + if (!fromId) return; + const fromIndex = songs.findIndex(s => s.id === fromId); + const toIndex = index; + if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return; + const ordered = [...songs]; + const [moved] = ordered.splice(fromIndex, 1); + ordered.splice(toIndex, 0, moved); + await onReorder(ordered.map(s => s.id)); + }} + onDragStartCapture={(e: React.DragEvent) => { + // Provide a simple id for intra-list reorder + if (!currentPlaylist || selectedSongs.size > 0) return; + e.dataTransfer.setData('text/song-id', song.id); + dragIndexRef.current = index; + }} /> )); }, [songs, selectedSongs, selectedSongId, toggleSelection, depth, onPlaySong, handleDragStart]); // Removed handleSongSelect since it's already memoized diff --git a/packages/frontend/src/services/api.ts b/packages/frontend/src/services/api.ts index 55f0a82..bdf7c7f 100644 --- a/packages/frontend/src/services/api.ts +++ b/packages/frontend/src/services/api.ts @@ -97,6 +97,15 @@ class Api { return response.json(); } + async reorderPlaylist(playlistName: string, orderedIds: string[]): Promise { + const response = await fetch(`${API_BASE_URL}/playlists/reorder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ playlistName, orderedIds }) + }); + if (!response.ok) throw new Error('Failed to reorder playlist'); + } + async resetDatabase(): Promise { try { const response = await fetch(`${API_BASE_URL}/reset`, { From 50a486f6d86c152a6a9ad765b5a96ab86379af71 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:09:36 +0200 Subject: [PATCH 02/14] feat(playlist-reorder): enable intra-playlist row drag&drop with landing indicator; persist order via backend --- .../src/components/PaginatedSongList.tsx | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 5b2c169..40baf3f 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -51,7 +51,10 @@ const SongItem = memo<{ showCheckbox: boolean; onPlaySong?: (song: Song) => void; onDragStart: (e: React.DragEvent, songIdsFallback: string[]) => void; -}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart }) => { + onRowDragOver?: (e: React.DragEvent) => void; + onRowDrop?: (e: React.DragEvent) => void; + onRowDragStartCapture?: (e: React.DragEvent) => void; +}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart, onRowDragOver, onRowDrop, onRowDragStartCapture }) => { // Memoize the formatted duration to prevent recalculation const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]); const handleClick = useCallback(() => { @@ -89,6 +92,9 @@ const SongItem = memo<{ onDragStart={(e) => { onDragStart(e, [song.id]); }} + onDragOver={onRowDragOver} + onDrop={onRowDrop} + onDragStartCapture={onRowDragStartCapture} > {showCheckbox && ( = memo(({ const scrollContainerRef = useRef(null); const isTriggeringRef = useRef(false); const timeoutRef = useRef(null); - const dragIndexRef = useRef(null); + const [dragHoverIndex, setDragHoverIndex] = useState(null); // Store current values in refs to avoid stale closures const hasMoreRef = useRef(hasMore); @@ -267,11 +273,12 @@ export const PaginatedSongList: React.FC = memo(({ onPlaySong={onPlaySong} onDragStart={handleDragStart} // Simple playlist reordering within same list by dragging rows - onDragOver={(e: React.DragEvent) => { + onRowDragOver={(e: React.DragEvent) => { if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; e.preventDefault(); + setDragHoverIndex(index); }} - onDrop={async (e: React.DragEvent) => { + onRowDrop={async (e: React.DragEvent) => { if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; e.preventDefault(); const fromId = e.dataTransfer.getData('text/song-id'); @@ -283,12 +290,12 @@ export const PaginatedSongList: React.FC = memo(({ const [moved] = ordered.splice(fromIndex, 1); ordered.splice(toIndex, 0, moved); await onReorder(ordered.map(s => s.id)); + setDragHoverIndex(null); }} - onDragStartCapture={(e: React.DragEvent) => { + onRowDragStartCapture={(e: React.DragEvent) => { // Provide a simple id for intra-list reorder if (!currentPlaylist || selectedSongs.size > 0) return; e.dataTransfer.setData('text/song-id', song.id); - dragIndexRef.current = index; }} /> )); @@ -467,7 +474,16 @@ export const PaginatedSongList: React.FC = memo(({ id="song-list-container" > - {songItems} + + {songs.map((song, index) => ( + + {dragHoverIndex === index && ( + + )} + {songItems[index]} + + ))} + {/* Loading indicator for infinite scroll or playlist switching */} {(loading || isSwitchingPlaylist) && ( From 5a21243f5b66b86d1a4fb341ac363045fb4c1703 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:15:55 +0200 Subject: [PATCH 03/14] chore(reorder): add client debug logs; refresh playlist after reorder; backend keeps missing ids at end when reordering --- packages/backend/src/routes/playlists.ts | 6 +++++- packages/frontend/src/App.tsx | 9 +++------ .../src/components/PaginatedSongList.tsx | 19 ++++++++++++++++--- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index 9c98f3e..9ab4430 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -96,7 +96,11 @@ router.post('/reorder', async (req: Request, res: Response) => { if (node.type === 'playlist' && node.name === playlistName) { // Ensure unique order and only include known IDs const unique = Array.from(new Set(orderedIds)); - node.tracks = unique; + // If tracks exist, keep only ids present in unique, and append any tracks not included to preserve membership + const current: string[] = Array.isArray(node.tracks) ? node.tracks : []; + const orderedKnown = unique.filter(id => current.includes(id)); + const missing = current.filter(id => !unique.includes(id)); + node.tracks = [...orderedKnown, ...missing]; updated = true; return node; } diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index bd08496..caf677e 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -129,7 +129,8 @@ const RekordboxReader: React.FC = () => { totalDuration, loadNextPage, searchSongs, - searchQuery + searchQuery, + refresh } = usePaginatedSongs({ pageSize: 100, playlistName: currentPlaylist }); // Export library to XML @@ -656,11 +657,7 @@ const RekordboxReader: React.FC = () => { // Persist order in backend await api.reorderPlaylist(currentPlaylist, orderedIds); // Refresh the current playlist view - // Easiest: refetch page 1 to reflect new order - // Future: keep local order optimistic - // For now, force reload songs by navigating to same route - // and resetting pagination via usePaginatedSongs hook - // noop here as hook likely fetches by API sort; backend will serve correct order when playlist songs endpoint respects track order + refresh(); }} /> diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 40baf3f..42c98d2 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -282,20 +282,33 @@ export const PaginatedSongList: React.FC = memo(({ if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; e.preventDefault(); const fromId = e.dataTransfer.getData('text/song-id'); - if (!fromId) return; + if (!fromId) { + console.debug('[Reorder] missing fromId'); + return; + } const fromIndex = songs.findIndex(s => s.id === fromId); const toIndex = index; - if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return; + if (fromIndex < 0 || toIndex < 0) { + console.debug('[Reorder] invalid indexes', { fromIndex, toIndex }); + return; + } + if (fromIndex === toIndex) { + console.debug('[Reorder] same index drop'); + return; + } const ordered = [...songs]; const [moved] = ordered.splice(fromIndex, 1); ordered.splice(toIndex, 0, moved); - await onReorder(ordered.map(s => s.id)); + const orderedIds = ordered.map(s => s.id); + console.debug('[Reorder] persisting order', { fromIndex, toIndex, fromId, toId: songs[index].id }); + await onReorder(orderedIds); setDragHoverIndex(null); }} onRowDragStartCapture={(e: React.DragEvent) => { // Provide a simple id for intra-list reorder if (!currentPlaylist || selectedSongs.size > 0) return; e.dataTransfer.setData('text/song-id', song.id); + console.debug('[Reorder] drag start', { id: song.id, index }); }} /> )); From 5a396d774e30fb063483582abbf8ae0aa7db84f0 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:20:48 +0200 Subject: [PATCH 04/14] fix(playlists): markModified on children/tracks to persist order; API response confirms update --- packages/backend/src/routes/playlists.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index 9ab4430..5450c1a 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -112,6 +112,9 @@ router.post('/reorder', async (req: Request, res: Response) => { 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(); } @@ -119,7 +122,7 @@ router.post('/reorder', async (req: Request, res: Response) => { return res.status(404).json({ message: `Playlist "${playlistName}" not found` }); } - res.json({ message: 'Playlist order updated' }); + 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' }); From 61d4ca16debdb1b654ba32cb0c0307088d133ada Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:25:50 +0200 Subject: [PATCH 05/14] feat(playlists): track custom order in separate 'order' array; reading API honors 'order' while preserving 'tracks' --- packages/backend/src/models/Playlist.ts | 2 ++ packages/backend/src/routes/playlists.ts | 10 +++------- packages/backend/src/routes/songs.ts | 10 ++++++++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/models/Playlist.ts b/packages/backend/src/models/Playlist.ts index d9661d7..b88f11b 100644 --- a/packages/backend/src/models/Playlist.ts +++ b/packages/backend/src/models/Playlist.ts @@ -9,6 +9,8 @@ const playlistSchema = new mongoose.Schema({ required: true }, tracks: [String], + // Optional custom order of track IDs (display-only, not exported) + order: [String], children: [{ type: mongoose.Schema.Types.Mixed }] diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index 5450c1a..bb454d5 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -80,7 +80,7 @@ router.post('/batch', async (req, res) => { } }); -// Reorder tracks inside a playlist (by name) +// 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[] }; @@ -94,13 +94,9 @@ router.post('/reorder', async (req: Request, res: Response) => { const reorderNode = (node: any): any => { if (!node) return node; if (node.type === 'playlist' && node.name === playlistName) { - // Ensure unique order and only include known IDs + // Store order separately; do not mutate tracks const unique = Array.from(new Set(orderedIds)); - // If tracks exist, keep only ids present in unique, and append any tracks not included to preserve membership - const current: string[] = Array.isArray(node.tracks) ? node.tracks : []; - const orderedKnown = unique.filter(id => current.includes(id)); - const missing = current.filter(id => !unique.includes(id)); - node.tracks = [...orderedKnown, ...missing]; + node.order = unique; updated = true; return node; } diff --git a/packages/backend/src/routes/songs.ts b/packages/backend/src/routes/songs.ts index 88f7100..6b5b10a 100644 --- a/packages/backend/src/routes/songs.ts +++ b/packages/backend/src/routes/songs.ts @@ -110,8 +110,14 @@ router.get('/playlist/*', async (req: Request, res: Response) => { // Get all track IDs from the playlist (including nested playlists) const getAllTrackIds = (node: any): string[] => { - if (node.type === 'playlist' && node.tracks) { - return node.tracks; + if (node.type === 'playlist') { + const base = Array.isArray(node.tracks) ? node.tracks : []; + const order = Array.isArray(node.order) ? node.order : []; + if (order.length === 0) return base; + const setBase = new Set(base); + const orderedKnown = order.filter((id: string) => setBase.has(id)); + const missing = base.filter((id: string) => !order.includes(id)); + return [...orderedKnown, ...missing]; } if (node.type === 'folder' && node.children) { return node.children.flatMap((child: any) => getAllTrackIds(child)); From 8136bbb95926d0096de92d1647a30f96fdcb3045 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:30:58 +0200 Subject: [PATCH 06/14] fix(reorder): backend move endpoint and frontend use precise move to persist order reliably; refresh after move --- packages/backend/src/routes/playlists.ts | 57 +++++++++++++++++++ .../src/components/PaginatedSongList.tsx | 11 ++-- packages/frontend/src/services/api.ts | 9 +++ 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index bb454d5..aab14b3 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -125,4 +125,61 @@ router.post('/reorder', async (req: Request, res: Response) => { } }); +// 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); + + 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' }); + } +}); + export const playlistsRouter = router; \ No newline at end of file diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 42c98d2..b45839a 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -16,6 +16,7 @@ import { import { Search2Icon } from '@chakra-ui/icons'; import { FiPlay } from 'react-icons/fi'; import type { Song, PlaylistNode } from '../types/interfaces'; +import { api } from '../services/api'; import { formatDuration, formatTotalDuration } from '../utils/formatters'; import { useDebounce } from '../hooks/useDebounce'; import { PlaylistSelectionModal } from './PlaylistSelectionModal'; @@ -296,12 +297,10 @@ export const PaginatedSongList: React.FC = memo(({ console.debug('[Reorder] same index drop'); return; } - const ordered = [...songs]; - const [moved] = ordered.splice(fromIndex, 1); - ordered.splice(toIndex, 0, moved); - const orderedIds = ordered.map(s => s.id); - console.debug('[Reorder] persisting order', { fromIndex, toIndex, fromId, toId: songs[index].id }); - await onReorder(orderedIds); + const toId = songs[index].id; + // Simpler and more robust: instruct backend to move fromId before toId + await api.moveTrackInPlaylist(currentPlaylist, fromId, toId); + await onReorder(songs.map(s => s.id)); // trigger refresh via parent setDragHoverIndex(null); }} onRowDragStartCapture={(e: React.DragEvent) => { diff --git a/packages/frontend/src/services/api.ts b/packages/frontend/src/services/api.ts index bdf7c7f..559467f 100644 --- a/packages/frontend/src/services/api.ts +++ b/packages/frontend/src/services/api.ts @@ -106,6 +106,15 @@ class Api { if (!response.ok) throw new Error('Failed to reorder playlist'); } + async moveTrackInPlaylist(playlistName: string, fromId: string, toId?: string): Promise { + const response = await fetch(`${API_BASE_URL}/playlists/reorder-move`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ playlistName, fromId, toId }) + }); + if (!response.ok) throw new Error('Failed to move track in playlist'); + } + async resetDatabase(): Promise { try { const response = await fetch(`${API_BASE_URL}/reset`, { From 484d191201b107871b50318fa85b02c2605862f4 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:33:42 +0200 Subject: [PATCH 07/14] fix(playlist-order): honor custom order in playlist songs endpoint by slicing trackIds and mapping results --- packages/backend/src/routes/songs.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/routes/songs.ts b/packages/backend/src/routes/songs.ts index 6b5b10a..8be0122 100644 --- a/packages/backend/src/routes/songs.ts +++ b/packages/backend/src/routes/songs.ts @@ -172,13 +172,17 @@ router.get('/playlist/*', async (req: Request, res: Response) => { return total + seconds; }, 0); - // Get songs with pagination - const songs = await Song.find(query) - .sort({ title: 1 }) - .skip(skip) - .limit(limit) + // Get songs for this page in the exact playlist order + const pageStart = (page - 1) * limit; + const pageEnd = Math.min(pageStart + limit, trackIds.length); + const pageTrackIds = trackIds.slice(pageStart, pageEnd); + + const pageSongs = await Song.find({ id: { $in: pageTrackIds } }) .populate('s3File.musicFileId') .lean(); + const idToSong: Record = {}; + for (const s of pageSongs) idToSong[s.id] = s; + const songs = pageTrackIds.map(id => idToSong[id]).filter(Boolean); console.log(`Found ${songs.length} songs for playlist "${playlistName}" (${totalSongs} total), ${songs.filter((s: any) => s.s3File?.hasS3File).length} with S3 files`); From e2d6d5543396b9b9e4d286155565be5d5a9171c2 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:37:45 +0200 Subject: [PATCH 08/14] chore(reorder-debug): add server/client logs for move and playlist order slice to diagnose ordering issue --- packages/backend/src/routes/playlists.ts | 2 ++ packages/backend/src/routes/songs.ts | 4 +++- packages/frontend/src/components/PaginatedSongList.tsx | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index aab14b3..b1e6e40 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -154,6 +154,8 @@ router.post('/reorder-move', async (req: Request, res: Response) => { 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; diff --git a/packages/backend/src/routes/songs.ts b/packages/backend/src/routes/songs.ts index 8be0122..3e543f9 100644 --- a/packages/backend/src/routes/songs.ts +++ b/packages/backend/src/routes/songs.ts @@ -126,7 +126,8 @@ router.get('/playlist/*', async (req: Request, res: Response) => { }; const trackIds = getAllTrackIds(playlist); - console.log(`Found ${trackIds.length} tracks in playlist "${playlistName}"`); + console.log(`[PLAYLIST_ORDER] ${playlistName} tracks count:`, trackIds.length); + console.log('[PLAYLIST_ORDER] first 10 ids:', trackIds.slice(0, 10)); if (trackIds.length === 0) { return res.json({ @@ -177,6 +178,7 @@ router.get('/playlist/*', async (req: Request, res: Response) => { const pageEnd = Math.min(pageStart + limit, trackIds.length); const pageTrackIds = trackIds.slice(pageStart, pageEnd); + console.log('[PLAYLIST_ORDER] page', page, 'limit', limit, 'slice', pageStart, pageEnd, 'ids:', pageTrackIds); const pageSongs = await Song.find({ id: { $in: pageTrackIds } }) .populate('s3File.musicFileId') .lean(); diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index b45839a..fc96086 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -299,6 +299,7 @@ export const PaginatedSongList: React.FC = memo(({ } const toId = songs[index].id; // Simpler and more robust: instruct backend to move fromId before toId + console.debug('[Reorder] move request', { playlist: currentPlaylist, fromId, toId }); await api.moveTrackInPlaylist(currentPlaylist, fromId, toId); await onReorder(songs.map(s => s.id)); // trigger refresh via parent setDragHoverIndex(null); From 2d42c6da717956d5055f9dcb34105c56245fd6a3 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:47:20 +0200 Subject: [PATCH 09/14] fix(reorder): stop overwriting server move with stale client order; just refresh after move; improve dnd effectAllowed/dropEffect --- packages/frontend/src/App.tsx | 12 +++++------- .../frontend/src/components/PaginatedSongList.tsx | 3 +++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index caf677e..fd6cffa 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -652,13 +652,11 @@ const RekordboxReader: React.FC = () => { searchQuery={searchQuery} isSwitchingPlaylist={isSwitchingPlaylist} onPlaySong={handlePlaySong} - onReorder={async (orderedIds: string[]) => { - if (!currentPlaylist || currentPlaylist === 'All Songs') return; - // Persist order in backend - await api.reorderPlaylist(currentPlaylist, orderedIds); - // Refresh the current playlist view - refresh(); - }} + onReorder={async (_orderedIds: string[]) => { + if (!currentPlaylist || currentPlaylist === 'All Songs') return; + // Do not overwrite server's move result with stale order; just refresh + refresh(); + }} /> diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index fc96086..1d2b399 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -309,6 +309,9 @@ export const PaginatedSongList: React.FC = memo(({ if (!currentPlaylist || selectedSongs.size > 0) return; e.dataTransfer.setData('text/song-id', song.id); console.debug('[Reorder] drag start', { id: song.id, index }); + // Explicitly set effect to move for better UX + try { e.dataTransfer.effectAllowed = 'move'; } catch {} + try { e.dataTransfer.dropEffect = 'move'; } catch {} }} /> )); From d0a83a85f58730ad2b9ff7a421c3d35d64e0a2da Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:52:48 +0200 Subject: [PATCH 10/14] feat(reorder): add end-of-list drop zone to move track to end; show hover indicator --- .../src/components/PaginatedSongList.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 1d2b399..bd0da02 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -170,6 +170,7 @@ export const PaginatedSongList: React.FC = memo(({ const isTriggeringRef = useRef(false); const timeoutRef = useRef(null); const [dragHoverIndex, setDragHoverIndex] = useState(null); + const [endDropHover, setEndDropHover] = useState(false); // Store current values in refs to avoid stale closures const hasMoreRef = useRef(hasMore); @@ -500,6 +501,36 @@ export const PaginatedSongList: React.FC = memo(({ ))} + + {/* Drop zone to move item to end of playlist */} + {onReorder && currentPlaylist && selectedSongs.size === 0 && ( + { + e.preventDefault(); + setDragHoverIndex(null); + setEndDropHover(true); + try { e.dataTransfer.dropEffect = 'move'; } catch {} + }} + onDragLeave={() => setEndDropHover(false)} + onDrop={async (e: React.DragEvent) => { + e.preventDefault(); + const fromId = e.dataTransfer.getData('text/song-id'); + if (!fromId) return; + // Move to end: omit toId + console.debug('[Reorder] move to end request', { playlist: currentPlaylist, fromId }); + await api.moveTrackInPlaylist(currentPlaylist, fromId); + await onReorder(songs.map(s => s.id)); + setEndDropHover(false); + }} + position="relative" + height="28px" + mt={1} + > + {endDropHover && ( + + )} + + )} {/* Loading indicator for infinite scroll or playlist switching */} {(loading || isSwitchingPlaylist) && ( From ac52441dd18af89c4437198ef02eb8e8d29b7166 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:53:26 +0200 Subject: [PATCH 11/14] fix(reorder): only show end-of-list drop zone during active reorder drag; reset hover indicators on drag end --- .../frontend/src/components/PaginatedSongList.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index bd0da02..0c62a8b 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -171,6 +171,7 @@ export const PaginatedSongList: React.FC = memo(({ const timeoutRef = useRef(null); const [dragHoverIndex, setDragHoverIndex] = useState(null); const [endDropHover, setEndDropHover] = useState(false); + const [isReorderDragging, setIsReorderDragging] = useState(false); // Store current values in refs to avoid stale closures const hasMoreRef = useRef(hasMore); @@ -258,6 +259,9 @@ export const PaginatedSongList: React.FC = memo(({ const handleDragEnd = useCallback(() => { setIsDragging(false); + setIsReorderDragging(false); + setEndDropHover(false); + setDragHoverIndex(null); dragSelectionRef.current = null; }, []); @@ -280,7 +284,7 @@ export const PaginatedSongList: React.FC = memo(({ e.preventDefault(); setDragHoverIndex(index); }} - onRowDrop={async (e: React.DragEvent) => { + onRowDrop={async (e: React.DragEvent) => { if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; e.preventDefault(); const fromId = e.dataTransfer.getData('text/song-id'); @@ -303,7 +307,8 @@ export const PaginatedSongList: React.FC = memo(({ console.debug('[Reorder] move request', { playlist: currentPlaylist, fromId, toId }); await api.moveTrackInPlaylist(currentPlaylist, fromId, toId); await onReorder(songs.map(s => s.id)); // trigger refresh via parent - setDragHoverIndex(null); + setDragHoverIndex(null); + setIsReorderDragging(false); }} onRowDragStartCapture={(e: React.DragEvent) => { // Provide a simple id for intra-list reorder @@ -313,6 +318,7 @@ export const PaginatedSongList: React.FC = memo(({ // Explicitly set effect to move for better UX try { e.dataTransfer.effectAllowed = 'move'; } catch {} try { e.dataTransfer.dropEffect = 'move'; } catch {} + setIsReorderDragging(true); }} /> )); @@ -503,7 +509,7 @@ export const PaginatedSongList: React.FC = memo(({ {/* Drop zone to move item to end of playlist */} - {onReorder && currentPlaylist && selectedSongs.size === 0 && ( + {onReorder && currentPlaylist && selectedSongs.size === 0 && isReorderDragging && ( { e.preventDefault(); @@ -521,7 +527,9 @@ export const PaginatedSongList: React.FC = memo(({ await api.moveTrackInPlaylist(currentPlaylist, fromId); await onReorder(songs.map(s => s.id)); setEndDropHover(false); + setIsReorderDragging(false); }} + onDragEnd={handleDragEnd} position="relative" height="28px" mt={1} From 9249a5a4a759c7cf8433198922e94d7647977dde Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:55:06 +0200 Subject: [PATCH 12/14] feat(reorder): support dragging multiple selected songs to a specific position and to end (block move) --- packages/backend/src/routes/playlists.ts | 65 +++++++++++++++++++ .../src/components/PaginatedSongList.tsx | 48 +++++++++++--- packages/frontend/src/services/api.ts | 9 +++ 3 files changed, 113 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index b1e6e40..8ced19d 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -184,4 +184,69 @@ router.post('/reorder-move', async (req: Request, res: Response) => { } }); +// 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; \ No newline at end of file diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 0c62a8b..59994e4 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -287,12 +287,22 @@ export const PaginatedSongList: React.FC = memo(({ onRowDrop={async (e: React.DragEvent) => { if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; e.preventDefault(); - const fromId = e.dataTransfer.getData('text/song-id'); - if (!fromId) { + const fromId = e.dataTransfer.getData('text/song-id'); + const multiJson = e.dataTransfer.getData('application/json'); + let multiIds: string[] | null = null; + if (multiJson) { + try { + const parsed = JSON.parse(multiJson); + if (parsed && parsed.type === 'songs' && Array.isArray(parsed.songIds)) { + multiIds = parsed.songIds as string[]; + } + } catch {} + } + if (!fromId && !multiIds) { console.debug('[Reorder] missing fromId'); return; } - const fromIndex = songs.findIndex(s => s.id === fromId); + const fromIndex = fromId ? songs.findIndex(s => s.id === fromId) : -1; const toIndex = index; if (fromIndex < 0 || toIndex < 0) { console.debug('[Reorder] invalid indexes', { fromIndex, toIndex }); @@ -303,9 +313,14 @@ export const PaginatedSongList: React.FC = memo(({ return; } const toId = songs[index].id; - // Simpler and more robust: instruct backend to move fromId before toId - console.debug('[Reorder] move request', { playlist: currentPlaylist, fromId, toId }); - await api.moveTrackInPlaylist(currentPlaylist, fromId, toId); + // If multiple, move block; else move single + if (multiIds && multiIds.length > 0) { + console.debug('[Reorder] move many request', { playlist: currentPlaylist, fromIds: multiIds, toId }); + await api.moveTracksInPlaylist(currentPlaylist, multiIds, toId); + } else { + console.debug('[Reorder] move request', { playlist: currentPlaylist, fromId, toId }); + await api.moveTrackInPlaylist(currentPlaylist, fromId!, toId); + } await onReorder(songs.map(s => s.id)); // trigger refresh via parent setDragHoverIndex(null); setIsReorderDragging(false); @@ -521,10 +536,25 @@ export const PaginatedSongList: React.FC = memo(({ onDrop={async (e: React.DragEvent) => { e.preventDefault(); const fromId = e.dataTransfer.getData('text/song-id'); - if (!fromId) return; + const multiJson = e.dataTransfer.getData('application/json'); + let multiIds: string[] | null = null; + if (multiJson) { + try { + const parsed = JSON.parse(multiJson); + if (parsed && parsed.type === 'songs' && Array.isArray(parsed.songIds)) { + multiIds = parsed.songIds as string[]; + } + } catch {} + } + if (!fromId && !multiIds) return; // Move to end: omit toId - console.debug('[Reorder] move to end request', { playlist: currentPlaylist, fromId }); - await api.moveTrackInPlaylist(currentPlaylist, fromId); + if (multiIds && multiIds.length > 0) { + console.debug('[Reorder] move many to end request', { playlist: currentPlaylist, fromIds: multiIds }); + await api.moveTracksInPlaylist(currentPlaylist, multiIds); + } else { + console.debug('[Reorder] move to end request', { playlist: currentPlaylist, fromId }); + await api.moveTrackInPlaylist(currentPlaylist, fromId!); + } await onReorder(songs.map(s => s.id)); setEndDropHover(false); setIsReorderDragging(false); diff --git a/packages/frontend/src/services/api.ts b/packages/frontend/src/services/api.ts index 559467f..cd94009 100644 --- a/packages/frontend/src/services/api.ts +++ b/packages/frontend/src/services/api.ts @@ -115,6 +115,15 @@ class Api { if (!response.ok) throw new Error('Failed to move track in playlist'); } + async moveTracksInPlaylist(playlistName: string, fromIds: string[], toId?: string): Promise { + const response = await fetch(`${API_BASE_URL}/playlists/reorder-move-many`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ playlistName, fromIds, toId }) + }); + if (!response.ok) throw new Error('Failed to move tracks in playlist'); + } + async resetDatabase(): Promise { try { const response = await fetch(`${API_BASE_URL}/reset`, { From 5659dde54099145794e160ea4e1aa65dfde8ee3c Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:56:28 +0200 Subject: [PATCH 13/14] feat(export): honor custom playlist order overlay when exporting Rekordbox XML (Entries + TRACK sequence) --- packages/backend/src/services/xmlService.ts | 52 +++++++++++---------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/services/xmlService.ts b/packages/backend/src/services/xmlService.ts index 5ce6e3d..be0037a 100644 --- a/packages/backend/src/services/xmlService.ts +++ b/packages/backend/src/services/xmlService.ts @@ -2,6 +2,17 @@ import { create } from "xmlbuilder"; import { Song } from "../models/Song.js"; import { Playlist } from "../models/Playlist.js"; +// Compute effective playlist order: custom overlay (order) + remaining base tracks +const getEffectiveTrackIds = (node: any): string[] => { + const base: string[] = Array.isArray(node?.tracks) ? node.tracks : []; + const overlay: string[] = Array.isArray(node?.order) ? node.order : []; + if (overlay.length === 0) return base; + const setBase = new Set(base); + const orderedKnown = overlay.filter((id: string) => setBase.has(id)); + const missing = base.filter((id: string) => !overlay.includes(id)); + return [...orderedKnown, ...missing]; +}; + const buildXmlNode = (node: any): any => { const xmlNode: any = { '@Type': node.type === 'folder' ? '0' : '1', @@ -14,13 +25,12 @@ const buildXmlNode = (node: any): any => { xmlNode.NODE = node.children.map((child: any) => buildXmlNode(child)); } } else { - // For playlists, always include KeyType and Entries + // For playlists, include KeyType and Entries based on effective order + const effectiveIds = getEffectiveTrackIds(node); xmlNode['@KeyType'] = '0'; - // Set Entries to the actual number of tracks - xmlNode['@Entries'] = (node.tracks || []).length; - // Include TRACK elements if there are tracks - if (node.tracks && node.tracks.length > 0) { - xmlNode.TRACK = node.tracks.map((trackId: string) => ({ + xmlNode['@Entries'] = effectiveIds.length; + if (effectiveIds.length > 0) { + xmlNode.TRACK = effectiveIds.map((trackId: string) => ({ '@Key': trackId })); } @@ -110,15 +120,13 @@ const streamPlaylistNodeFull = async (res: any, node: any) => { res.write('\n '); } else { - const trackCount = node.tracks ? node.tracks.length : 0; - res.write(`\n `); - - if (node.tracks && node.tracks.length > 0) { - for (const trackId of node.tracks) { + const effectiveIds = getEffectiveTrackIds(node); + res.write(`\n `); + if (effectiveIds.length > 0) { + for (const trackId of effectiveIds) { res.write(`\n `); } } - res.write('\n '); } }; @@ -138,15 +146,13 @@ const streamPlaylistNodeCompact = async (res: any, node: any) => { res.write(''); } else { - const trackCount = node.tracks ? node.tracks.length : 0; - res.write(``); - - if (node.tracks && node.tracks.length > 0) { - for (const trackId of node.tracks) { + const effectiveIds = getEffectiveTrackIds(node); + res.write(``); + if (effectiveIds.length > 0) { + for (const trackId of effectiveIds) { res.write(``); } } - res.write(''); } }; @@ -167,15 +173,13 @@ const streamPlaylistNode = async (res: any, node: any, indent: number) => { res.write(`${spaces}\n`); } else { - const trackCount = node.tracks ? node.tracks.length : 0; - res.write(`${spaces}\n`); - - if (node.tracks && node.tracks.length > 0) { - for (const trackId of node.tracks) { + const effectiveIds = getEffectiveTrackIds(node); + res.write(`${spaces}\n`); + if (effectiveIds.length > 0) { + for (const trackId of effectiveIds) { res.write(`${spaces} \n`); } } - res.write(`${spaces}\n`); } }; From 5f173808163ba7def3393dc6b7e6c76b8bd120c3 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 15:05:10 +0200 Subject: [PATCH 14/14] chore: remove verbose reorder/debug logs and tidy XML export logs; streamline FE reorder console noise --- packages/backend/src/routes/playlists.ts | 6 ++--- packages/backend/src/routes/songs.ts | 12 +++------- packages/backend/src/services/xmlService.ts | 8 ++----- .../src/components/PaginatedSongList.tsx | 22 ++++--------------- 4 files changed, 11 insertions(+), 37 deletions(-) diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index 8ced19d..448c121 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -154,8 +154,7 @@ router.post('/reorder-move', async (req: Request, res: Response) => { 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)); + // Updated playlist order overlay node.order = without; // store full order overlay updated = true; return node; @@ -219,8 +218,7 @@ router.post('/reorder-move-many', async (req: Request, res: Response) => { // 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)); + // Updated playlist order overlay for batch move node.order = without; updated = true; return node; diff --git a/packages/backend/src/routes/songs.ts b/packages/backend/src/routes/songs.ts index 3e543f9..184ebec 100644 --- a/packages/backend/src/routes/songs.ts +++ b/packages/backend/src/routes/songs.ts @@ -76,7 +76,7 @@ router.get('/playlist/*', async (req: Request, res: Response) => { const search = req.query.search as string || ''; const skip = (page - 1) * limit; - console.log(`Fetching songs for playlist "${playlistName}"... Page: ${page}, Limit: ${limit}, Search: "${search}"`); + // Fetch songs for playlist with pagination // Find the playlist recursively in the playlist structure const findPlaylistRecursively = (nodes: any[], targetName: string): any => { @@ -102,9 +102,7 @@ router.get('/playlist/*', async (req: Request, res: Response) => { } if (!playlist) { - console.log(`Playlist "${playlistName}" not found. Available playlists:`); - const allPlaylistNames = await Playlist.find({}, 'name'); - console.log(allPlaylistNames.map(p => p.name)); + // Playlist not found return res.status(404).json({ message: `Playlist "${playlistName}" not found` }); } @@ -126,8 +124,7 @@ router.get('/playlist/*', async (req: Request, res: Response) => { }; const trackIds = getAllTrackIds(playlist); - console.log(`[PLAYLIST_ORDER] ${playlistName} tracks count:`, trackIds.length); - console.log('[PLAYLIST_ORDER] first 10 ids:', trackIds.slice(0, 10)); + // Compute effective ordered track IDs if (trackIds.length === 0) { return res.json({ @@ -178,7 +175,6 @@ router.get('/playlist/*', async (req: Request, res: Response) => { const pageEnd = Math.min(pageStart + limit, trackIds.length); const pageTrackIds = trackIds.slice(pageStart, pageEnd); - console.log('[PLAYLIST_ORDER] page', page, 'limit', limit, 'slice', pageStart, pageEnd, 'ids:', pageTrackIds); const pageSongs = await Song.find({ id: { $in: pageTrackIds } }) .populate('s3File.musicFileId') .lean(); @@ -186,8 +182,6 @@ router.get('/playlist/*', async (req: Request, res: Response) => { for (const s of pageSongs) idToSong[s.id] = s; const songs = pageTrackIds.map(id => idToSong[id]).filter(Boolean); - console.log(`Found ${songs.length} songs for playlist "${playlistName}" (${totalSongs} total), ${songs.filter((s: any) => s.s3File?.hasS3File).length} with S3 files`); - res.json({ songs, pagination: { diff --git a/packages/backend/src/services/xmlService.ts b/packages/backend/src/services/xmlService.ts index be0037a..2f2a92b 100644 --- a/packages/backend/src/services/xmlService.ts +++ b/packages/backend/src/services/xmlService.ts @@ -40,7 +40,7 @@ const buildXmlNode = (node: any): any => { }; export const streamToXml = async (res: any) => { - console.log('Starting streamToXml function...'); + console.log('Starting XML export...'); // Write XML header with encoding (like master collection) res.write(''); @@ -50,9 +50,7 @@ export const streamToXml = async (res: any) => { res.write('\n '); // Start COLLECTION section - console.log('Counting songs in database...'); const songCount = await Song.countDocuments(); - console.log(`Found ${songCount} songs in database`); res.write(`\n `); // Stream songs in batches to avoid memory issues @@ -78,7 +76,6 @@ export const streamToXml = async (res: any) => { } processedSongs += songs.length; - console.log(`Streamed ${processedSongs}/${songCount} songs...`); } res.write('\n '); @@ -87,9 +84,7 @@ export const streamToXml = async (res: any) => { res.write('\n '); // Stream playlists - console.log('Fetching playlists from database...'); const playlists = await Playlist.find({}).lean(); - console.log(`Found ${playlists.length} playlists in database`); // Write ROOT node with correct Count res.write(`\n `); @@ -103,6 +98,7 @@ export const streamToXml = async (res: any) => { res.write('\n'); res.end(); + console.log('XML export completed.'); }; const streamPlaylistNodeFull = async (res: any, node: any) => { diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 59994e4..cb81877 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -298,27 +298,16 @@ export const PaginatedSongList: React.FC = memo(({ } } catch {} } - if (!fromId && !multiIds) { - console.debug('[Reorder] missing fromId'); - return; - } + if (!fromId && !multiIds) return; const fromIndex = fromId ? songs.findIndex(s => s.id === fromId) : -1; const toIndex = index; - if (fromIndex < 0 || toIndex < 0) { - console.debug('[Reorder] invalid indexes', { fromIndex, toIndex }); - return; - } - if (fromIndex === toIndex) { - console.debug('[Reorder] same index drop'); - return; - } + if (fromIndex < 0 || toIndex < 0) return; + if (fromIndex === toIndex) return; const toId = songs[index].id; // If multiple, move block; else move single if (multiIds && multiIds.length > 0) { - console.debug('[Reorder] move many request', { playlist: currentPlaylist, fromIds: multiIds, toId }); await api.moveTracksInPlaylist(currentPlaylist, multiIds, toId); } else { - console.debug('[Reorder] move request', { playlist: currentPlaylist, fromId, toId }); await api.moveTrackInPlaylist(currentPlaylist, fromId!, toId); } await onReorder(songs.map(s => s.id)); // trigger refresh via parent @@ -326,10 +315,9 @@ export const PaginatedSongList: React.FC = memo(({ setIsReorderDragging(false); }} onRowDragStartCapture={(e: React.DragEvent) => { - // Provide a simple id for intra-list reorder + // Provide a simple id for intra-list reorder if (!currentPlaylist || selectedSongs.size > 0) return; e.dataTransfer.setData('text/song-id', song.id); - console.debug('[Reorder] drag start', { id: song.id, index }); // Explicitly set effect to move for better UX try { e.dataTransfer.effectAllowed = 'move'; } catch {} try { e.dataTransfer.dropEffect = 'move'; } catch {} @@ -549,10 +537,8 @@ export const PaginatedSongList: React.FC = memo(({ if (!fromId && !multiIds) return; // Move to end: omit toId if (multiIds && multiIds.length > 0) { - console.debug('[Reorder] move many to end request', { playlist: currentPlaylist, fromIds: multiIds }); await api.moveTracksInPlaylist(currentPlaylist, multiIds); } else { - console.debug('[Reorder] move to end request', { playlist: currentPlaylist, fromId }); await api.moveTrackInPlaylist(currentPlaylist, fromId!); } await onReorder(songs.map(s => s.id));