diff --git a/packages/backend/src/routes/songs.ts b/packages/backend/src/routes/songs.ts index dec7227..4681cb0 100644 --- a/packages/backend/src/routes/songs.ts +++ b/packages/backend/src/routes/songs.ts @@ -148,6 +148,15 @@ router.get('/playlist/*', async (req: Request, res: Response) => { const totalSongs = await Song.countDocuments(query); const totalPages = Math.ceil(totalSongs / limit); + // Calculate total duration for the entire playlist + const allPlaylistSongs = await Song.find({ id: { $in: trackIds } }).lean(); + const totalDuration = allPlaylistSongs.reduce((total, song: any) => { + if (!song.totalTime) return total; + const totalTimeStr = String(song.totalTime); + const seconds = Math.floor(Number(totalTimeStr) / (totalTimeStr.length > 4 ? 1000 : 1)); + return total + seconds; + }, 0); + // Get songs with pagination const songs = await Song.find(query) .sort({ title: 1 }) @@ -166,7 +175,8 @@ router.get('/playlist/*', async (req: Request, res: Response) => { totalPages, hasNextPage: page < totalPages, hasPrevPage: page > 1 - } + }, + totalDuration: totalDuration }); } catch (error) { console.error('Error fetching playlist songs:', error); diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index e40cea2..0fa2c63 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { SongDetails } from "./components/SongDetails"; import { Configuration } from "./pages/Configuration"; import { useXmlParser } from "./hooks/useXmlParser"; import { usePaginatedSongs } from "./hooks/usePaginatedSongs"; +import { formatTotalDuration } from "./utils/formatters"; import { api } from "./services/api"; import type { Song, PlaylistNode } from "./types/interfaces"; @@ -56,15 +57,15 @@ const findPlaylistByName = (playlists: PlaylistNode[], name: string): PlaylistNo return null; }; -const getAllPlaylistTracks = (node: PlaylistNode): string[] => { - if (!node.type || node.type === 'playlist') { // Handle both old and new playlist formats - return node.tracks || []; - } - if (node.type === 'folder' && node.children) { - return node.children.flatMap(child => getAllPlaylistTracks(child)); - } - return []; -}; + const getAllPlaylistTracks = (node: PlaylistNode): string[] => { + if (!node.type || node.type === 'playlist') { // Handle both old and new playlist formats + return node.tracks || []; + } + if (node.type === 'folder' && node.children) { + return node.children.flatMap(child => getAllPlaylistTracks(child)); + } + return []; + }; export default function RekordboxReader() { const { playlists, setPlaylists, loading: xmlLoading } = useXmlParser(); @@ -74,6 +75,13 @@ export default function RekordboxReader() { const handleSongSelect = useCallback((song: Song) => { setSelectedSong(song); }, []); + + // Format total duration for display + const getFormattedTotalDuration = useCallback((durationSeconds?: number): string => { + if (!durationSeconds) return ""; + return formatTotalDuration(durationSeconds); + }, []); + const navigate = useNavigate(); const location = useLocation(); const initialLoadDone = useRef(false); @@ -96,6 +104,7 @@ export default function RekordboxReader() { loading: songsLoading, hasMore, totalSongs, + totalDuration, loadNextPage, searchSongs, searchQuery @@ -505,6 +514,7 @@ export default function RekordboxReader() { loading={songsLoading} hasMore={hasMore} totalSongs={totalSongs} + totalPlaylistDuration={getFormattedTotalDuration(totalDuration)} onLoadMore={loadNextPage} onSearch={searchSongs} searchQuery={searchQuery} diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index ae98be4..1430296 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -34,6 +34,7 @@ interface PaginatedSongListProps { loading: boolean; hasMore: boolean; totalSongs: number; + totalPlaylistDuration?: string; // Total duration of the entire playlist onLoadMore: () => void; onSearch: (query: string) => void; searchQuery: string; @@ -111,6 +112,7 @@ export const PaginatedSongList: React.FC = memo(({ loading, hasMore, totalSongs, + totalPlaylistDuration, onLoadMore, onSearch, searchQuery, @@ -213,15 +215,19 @@ export const PaginatedSongList: React.FC = memo(({ )); }, [songs, selectedSongs, selectedSongId, handleSongSelect, toggleSelection, depth]); - // Memoized total duration calculation + // Use total playlist duration if available, otherwise calculate from current songs const totalDuration = useMemo(() => { + if (totalPlaylistDuration) { + return totalPlaylistDuration; + } + // Fallback to calculating from current songs const totalSeconds = songs.reduce((total, song) => { if (!song.totalTime) return total; const seconds = Math.floor(Number(song.totalTime) / (song.totalTime.length > 4 ? 1000 : 1)); return total + seconds; }, 0); return formatTotalDuration(totalSeconds); - }, [songs]); + }, [songs, totalPlaylistDuration]); // Memoized playlist options for bulk actions const playlistOptions = useMemo(() => { diff --git a/packages/frontend/src/hooks/usePaginatedSongs.ts b/packages/frontend/src/hooks/usePaginatedSongs.ts index 7a291bc..537564e 100644 --- a/packages/frontend/src/hooks/usePaginatedSongs.ts +++ b/packages/frontend/src/hooks/usePaginatedSongs.ts @@ -18,6 +18,7 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => { const [currentPage, setCurrentPage] = useState(1); const [searchQuery, setSearchQuery] = useState(initialSearch); const [totalSongs, setTotalSongs] = useState(0); + const [totalDuration, setTotalDuration] = useState(undefined); const [isInitialLoad, setIsInitialLoad] = useState(true); const loadingRef = useRef(false); @@ -76,6 +77,7 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => { setHasMore(response.pagination.hasNextPage); setTotalSongs(response.pagination.totalSongs); + setTotalDuration(response.totalDuration); setCurrentPage(page); } catch (err) { // Don't set error if request was aborted @@ -156,6 +158,7 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => { error, hasMore, totalSongs, + totalDuration, currentPage, searchQuery, isInitialLoad, diff --git a/packages/frontend/src/services/api.ts b/packages/frontend/src/services/api.ts index 29f81cc..5f41378 100644 --- a/packages/frontend/src/services/api.ts +++ b/packages/frontend/src/services/api.ts @@ -14,6 +14,7 @@ export interface PaginationInfo { export interface SongsResponse { songs: Song[]; pagination: PaginationInfo; + totalDuration?: number; // Total duration of the entire playlist in seconds } class Api {