diff --git a/backend/src/routes/library.ts b/backend/src/routes/library.ts index a2b0e3f..f370173 100644 --- a/backend/src/routes/library.ts +++ b/backend/src/routes/library.ts @@ -693,7 +693,7 @@ router.get("/artists", async (req, res) => { offset: offsetParam = "0", filter = "owned", // owned (default), discovery, all } = req.query; - const limit = Math.min(parseInt(limitParam as string, 10) || 500, 1000); // Max 1000 + const limit = parseInt(limitParam as string, 10) || 500; // No max cap - support unlimited pagination const offset = parseInt(offsetParam as string, 10) || 0; // Build where clause based on filter @@ -1577,7 +1577,7 @@ router.get("/albums", async (req, res) => { offset: offsetParam = "0", filter = "owned", // owned (default), discovery, all } = req.query; - const limit = Math.min(parseInt(limitParam as string, 10) || 500, 1000); // Max 1000 + const limit = parseInt(limitParam as string, 10) || 500; // No max cap - support unlimited pagination const offset = parseInt(offsetParam as string, 10) || 0; let where: any = { @@ -1721,34 +1721,39 @@ router.get("/albums/:id", async (req, res) => { } }); -// GET /library/tracks?albumId=&limit=100 +// GET /library/tracks?albumId=&limit=100&offset=0 router.get("/tracks", async (req, res) => { try { - const { albumId, limit = "100" } = req.query; - const limitNum = parseInt(limit as string, 10); + const { albumId, limit: limitParam = "100", offset: offsetParam = "0" } = req.query; + const limit = parseInt(limitParam as string, 10) || 100; + const offset = parseInt(offsetParam as string, 10) || 0; const where: any = {}; if (albumId) { where.albumId = albumId as string; } - const tracksData = await prisma.track.findMany({ - where, - take: limitNum, - orderBy: albumId ? { trackNo: "asc" } : { id: "desc" }, - include: { - album: { - include: { - artist: { - select: { - id: true, - name: true, + const [tracksData, total] = await Promise.all([ + prisma.track.findMany({ + where, + skip: offset, + take: limit, + orderBy: albumId ? { trackNo: "asc" } : { id: "desc" }, + include: { + album: { + include: { + artist: { + select: { + id: true, + name: true, + }, }, }, }, }, - }, - }); + }), + prisma.track.count({ where }), + ]); // Add coverArt field to albums const tracks = tracksData.map((track) => ({ @@ -1759,7 +1764,7 @@ router.get("/tracks", async (req, res) => { }, })); - res.json({ tracks }); + res.json({ tracks, total, offset, limit }); } catch (error) { console.error("Get tracks error:", error); res.status(500).json({ error: "Failed to fetch tracks" }); diff --git a/frontend/app/library/page.tsx b/frontend/app/library/page.tsx index 4067eba..383ba17 100644 --- a/frontend/app/library/page.tsx +++ b/frontend/app/library/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useMemo, useEffect } from "react"; +import { useState, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useAudio } from "@/lib/audio-context"; import { ConfirmDialog } from "@/components/ui/ConfirmDialog"; @@ -8,6 +8,7 @@ import { Tab, DeleteDialogState } from "@/features/library/types"; import { useLibraryData, LibraryFilter, + SortOption, } from "@/features/library/hooks/useLibraryData"; import { api } from "@/lib/api"; import { useLibraryActions } from "@/features/library/hooks/useLibraryActions"; @@ -18,8 +19,6 @@ import { AlbumsGrid } from "@/features/library/components/AlbumsGrid"; import { TracksList } from "@/features/library/components/TracksList"; import { Shuffle, ListFilter } from "lucide-react"; -type SortOption = "name" | "name-desc" | "recent" | "tracks"; - export default function LibraryPage() { const router = useRouter(); const searchParams = useSearchParams(); @@ -37,10 +36,13 @@ export default function LibraryPage() { const [currentPage, setCurrentPage] = useState(1); const [showFilters, setShowFilters] = useState(false); - // Use custom hooks - const { artists, albums, tracks, isLoading, reloadData } = useLibraryData({ + // Use custom hooks with server-side pagination + const { artists, albums, tracks, isLoading, reloadData, pagination } = useLibraryData({ activeTab, filter, + sortBy, + itemsPerPage, + currentPage, }); const { playArtist, @@ -61,84 +63,14 @@ export default function LibraryPage() { } }, [activeTab]); - // Sort and paginate data - const sortedArtists = useMemo(() => { - const sorted = [...artists]; - switch (sortBy) { - case "name": - sorted.sort((a, b) => a.name.localeCompare(b.name)); - break; - case "name-desc": - sorted.sort((a, b) => b.name.localeCompare(a.name)); - break; - case "tracks": - sorted.sort( - (a, b) => (b.trackCount || 0) - (a.trackCount || 0) - ); - break; - default: - sorted.sort((a, b) => a.name.localeCompare(b.name)); - } - return sorted; - }, [artists, sortBy]); + // Reset page when filter or sort changes + useEffect(() => { + setCurrentPage(1); + }, [filter, sortBy, itemsPerPage]); - const sortedAlbums = useMemo(() => { - const sorted = [...albums]; - switch (sortBy) { - case "name": - sorted.sort((a, b) => a.title.localeCompare(b.title)); - break; - case "name-desc": - sorted.sort((a, b) => b.title.localeCompare(a.title)); - break; - case "recent": - sorted.sort((a, b) => (b.year || 0) - (a.year || 0)); - break; - default: - sorted.sort((a, b) => a.title.localeCompare(b.title)); - } - return sorted; - }, [albums, sortBy]); - - const sortedTracks = useMemo(() => { - const sorted = [...tracks]; - switch (sortBy) { - case "name": - sorted.sort((a, b) => a.title.localeCompare(b.title)); - break; - case "name-desc": - sorted.sort((a, b) => b.title.localeCompare(a.title)); - break; - default: - sorted.sort((a, b) => a.title.localeCompare(b.title)); - } - return sorted; - }, [tracks, sortBy]); - - // Paginate - const paginatedArtists = useMemo(() => { - const start = (currentPage - 1) * itemsPerPage; - return sortedArtists.slice(start, start + itemsPerPage); - }, [sortedArtists, currentPage, itemsPerPage]); - - const paginatedAlbums = useMemo(() => { - const start = (currentPage - 1) * itemsPerPage; - return sortedAlbums.slice(start, start + itemsPerPage); - }, [sortedAlbums, currentPage, itemsPerPage]); - - const paginatedTracks = useMemo(() => { - const start = (currentPage - 1) * itemsPerPage; - return sortedTracks.slice(start, start + itemsPerPage); - }, [sortedTracks, currentPage, itemsPerPage]); - - // Total counts and pages - const totalItems = - activeTab === "artists" - ? artists.length - : activeTab === "albums" - ? albums.length - : tracks.length; - const totalPages = Math.ceil(totalItems / itemsPerPage); + // Get total items and pages from pagination + const totalItems = pagination.total; + const totalPages = pagination.totalPages; // Delete confirmation dialog state const [deleteConfirm, setDeleteConfirm] = useState({ @@ -180,17 +112,13 @@ export default function LibraryPage() { playTracks(formattedTracks, startIndex); }; - // Shuffle entire library + // Shuffle entire library - fetches all tracks for true shuffle const handleShuffleLibrary = async () => { try { - // Get all tracks if not already loaded - let allTracks = tracks; - if (activeTab !== "tracks" || tracks.length === 0) { - const { tracks: fetchedTracks } = await api.getTracks({ - limit: 1000, - }); - allTracks = fetchedTracks; - } + // Fetch a large batch of tracks for shuffling + const { tracks: allTracks } = await api.getTracks({ + limit: 10000, + }); if (allTracks.length === 0) { return; @@ -271,7 +199,7 @@ export default function LibraryPage() { {/* Item Count */} - {totalItems}{" "} + {totalItems.toLocaleString()}{" "} {activeTab === "artists" ? "artists" : activeTab === "albums" @@ -289,10 +217,7 @@ export default function LibraryPage() { activeTab === "albums") && (