feat: Show total playlist duration instead of loaded songs duration - Add totalDuration calculation in backend playlist endpoint - Update frontend to display total playlist duration - Duration now shows entire playlist length, not just loaded songs - Prevents duration from changing when scrolling through songs

This commit is contained in:
Geert Rademakes 2025-08-06 10:30:02 +02:00
parent 54bffbc25d
commit 02ae12294c
5 changed files with 42 additions and 12 deletions

View File

@ -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);

View File

@ -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";
@ -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}

View File

@ -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<PaginatedSongListProps> = memo(({
loading,
hasMore,
totalSongs,
totalPlaylistDuration,
onLoadMore,
onSearch,
searchQuery,
@ -213,15 +215,19 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = 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(() => {

View File

@ -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<number | undefined>(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,

View File

@ -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 {