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:
parent
54bffbc25d
commit
02ae12294c
@ -148,6 +148,15 @@ router.get('/playlist/*', async (req: Request, res: Response) => {
|
|||||||
const totalSongs = await Song.countDocuments(query);
|
const totalSongs = await Song.countDocuments(query);
|
||||||
const totalPages = Math.ceil(totalSongs / limit);
|
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
|
// Get songs with pagination
|
||||||
const songs = await Song.find(query)
|
const songs = await Song.find(query)
|
||||||
.sort({ title: 1 })
|
.sort({ title: 1 })
|
||||||
@ -166,7 +175,8 @@ router.get('/playlist/*', async (req: Request, res: Response) => {
|
|||||||
totalPages,
|
totalPages,
|
||||||
hasNextPage: page < totalPages,
|
hasNextPage: page < totalPages,
|
||||||
hasPrevPage: page > 1
|
hasPrevPage: page > 1
|
||||||
}
|
},
|
||||||
|
totalDuration: totalDuration
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching playlist songs:', error);
|
console.error('Error fetching playlist songs:', error);
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { SongDetails } from "./components/SongDetails";
|
|||||||
import { Configuration } from "./pages/Configuration";
|
import { Configuration } from "./pages/Configuration";
|
||||||
import { useXmlParser } from "./hooks/useXmlParser";
|
import { useXmlParser } from "./hooks/useXmlParser";
|
||||||
import { usePaginatedSongs } from "./hooks/usePaginatedSongs";
|
import { usePaginatedSongs } from "./hooks/usePaginatedSongs";
|
||||||
|
import { formatTotalDuration } from "./utils/formatters";
|
||||||
|
|
||||||
import { api } from "./services/api";
|
import { api } from "./services/api";
|
||||||
import type { Song, PlaylistNode } from "./types/interfaces";
|
import type { Song, PlaylistNode } from "./types/interfaces";
|
||||||
@ -56,7 +57,7 @@ const findPlaylistByName = (playlists: PlaylistNode[], name: string): PlaylistNo
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllPlaylistTracks = (node: PlaylistNode): string[] => {
|
const getAllPlaylistTracks = (node: PlaylistNode): string[] => {
|
||||||
if (!node.type || node.type === 'playlist') { // Handle both old and new playlist formats
|
if (!node.type || node.type === 'playlist') { // Handle both old and new playlist formats
|
||||||
return node.tracks || [];
|
return node.tracks || [];
|
||||||
}
|
}
|
||||||
@ -64,7 +65,7 @@ const getAllPlaylistTracks = (node: PlaylistNode): string[] => {
|
|||||||
return node.children.flatMap(child => getAllPlaylistTracks(child));
|
return node.children.flatMap(child => getAllPlaylistTracks(child));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RekordboxReader() {
|
export default function RekordboxReader() {
|
||||||
const { playlists, setPlaylists, loading: xmlLoading } = useXmlParser();
|
const { playlists, setPlaylists, loading: xmlLoading } = useXmlParser();
|
||||||
@ -74,6 +75,13 @@ export default function RekordboxReader() {
|
|||||||
const handleSongSelect = useCallback((song: Song) => {
|
const handleSongSelect = useCallback((song: Song) => {
|
||||||
setSelectedSong(song);
|
setSelectedSong(song);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Format total duration for display
|
||||||
|
const getFormattedTotalDuration = useCallback((durationSeconds?: number): string => {
|
||||||
|
if (!durationSeconds) return "";
|
||||||
|
return formatTotalDuration(durationSeconds);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const initialLoadDone = useRef(false);
|
const initialLoadDone = useRef(false);
|
||||||
@ -96,6 +104,7 @@ export default function RekordboxReader() {
|
|||||||
loading: songsLoading,
|
loading: songsLoading,
|
||||||
hasMore,
|
hasMore,
|
||||||
totalSongs,
|
totalSongs,
|
||||||
|
totalDuration,
|
||||||
loadNextPage,
|
loadNextPage,
|
||||||
searchSongs,
|
searchSongs,
|
||||||
searchQuery
|
searchQuery
|
||||||
@ -505,6 +514,7 @@ export default function RekordboxReader() {
|
|||||||
loading={songsLoading}
|
loading={songsLoading}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
totalSongs={totalSongs}
|
totalSongs={totalSongs}
|
||||||
|
totalPlaylistDuration={getFormattedTotalDuration(totalDuration)}
|
||||||
onLoadMore={loadNextPage}
|
onLoadMore={loadNextPage}
|
||||||
onSearch={searchSongs}
|
onSearch={searchSongs}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ interface PaginatedSongListProps {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
totalSongs: number;
|
totalSongs: number;
|
||||||
|
totalPlaylistDuration?: string; // Total duration of the entire playlist
|
||||||
onLoadMore: () => void;
|
onLoadMore: () => void;
|
||||||
onSearch: (query: string) => void;
|
onSearch: (query: string) => void;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@ -111,6 +112,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
loading,
|
loading,
|
||||||
hasMore,
|
hasMore,
|
||||||
totalSongs,
|
totalSongs,
|
||||||
|
totalPlaylistDuration,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
onSearch,
|
onSearch,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@ -213,15 +215,19 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
));
|
));
|
||||||
}, [songs, selectedSongs, selectedSongId, handleSongSelect, toggleSelection, depth]);
|
}, [songs, selectedSongs, selectedSongId, handleSongSelect, toggleSelection, depth]);
|
||||||
|
|
||||||
// Memoized total duration calculation
|
// Use total playlist duration if available, otherwise calculate from current songs
|
||||||
const totalDuration = useMemo(() => {
|
const totalDuration = useMemo(() => {
|
||||||
|
if (totalPlaylistDuration) {
|
||||||
|
return totalPlaylistDuration;
|
||||||
|
}
|
||||||
|
// Fallback to calculating from current songs
|
||||||
const totalSeconds = songs.reduce((total, song) => {
|
const totalSeconds = songs.reduce((total, song) => {
|
||||||
if (!song.totalTime) return total;
|
if (!song.totalTime) return total;
|
||||||
const seconds = Math.floor(Number(song.totalTime) / (song.totalTime.length > 4 ? 1000 : 1));
|
const seconds = Math.floor(Number(song.totalTime) / (song.totalTime.length > 4 ? 1000 : 1));
|
||||||
return total + seconds;
|
return total + seconds;
|
||||||
}, 0);
|
}, 0);
|
||||||
return formatTotalDuration(totalSeconds);
|
return formatTotalDuration(totalSeconds);
|
||||||
}, [songs]);
|
}, [songs, totalPlaylistDuration]);
|
||||||
|
|
||||||
// Memoized playlist options for bulk actions
|
// Memoized playlist options for bulk actions
|
||||||
const playlistOptions = useMemo(() => {
|
const playlistOptions = useMemo(() => {
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [searchQuery, setSearchQuery] = useState(initialSearch);
|
const [searchQuery, setSearchQuery] = useState(initialSearch);
|
||||||
const [totalSongs, setTotalSongs] = useState(0);
|
const [totalSongs, setTotalSongs] = useState(0);
|
||||||
|
const [totalDuration, setTotalDuration] = useState<number | undefined>(undefined);
|
||||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||||
|
|
||||||
const loadingRef = useRef(false);
|
const loadingRef = useRef(false);
|
||||||
@ -76,6 +77,7 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
|
|||||||
|
|
||||||
setHasMore(response.pagination.hasNextPage);
|
setHasMore(response.pagination.hasNextPage);
|
||||||
setTotalSongs(response.pagination.totalSongs);
|
setTotalSongs(response.pagination.totalSongs);
|
||||||
|
setTotalDuration(response.totalDuration);
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Don't set error if request was aborted
|
// Don't set error if request was aborted
|
||||||
@ -156,6 +158,7 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
|
|||||||
error,
|
error,
|
||||||
hasMore,
|
hasMore,
|
||||||
totalSongs,
|
totalSongs,
|
||||||
|
totalDuration,
|
||||||
currentPage,
|
currentPage,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
isInitialLoad,
|
isInitialLoad,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export interface PaginationInfo {
|
|||||||
export interface SongsResponse {
|
export interface SongsResponse {
|
||||||
songs: Song[];
|
songs: Song[];
|
||||||
pagination: PaginationInfo;
|
pagination: PaginationInfo;
|
||||||
|
totalDuration?: number; // Total duration of the entire playlist in seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user