From 4f6c5995f8eda6a7eb2f94f97f860d5755a6ee24 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sun, 12 Jul 2026 15:24:16 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20Library=20page=20=E2=80=94=20owned?= =?UTF-8?q?-albums=20grid=20with=20cover=20art?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New /library: GET /api/library lists LibraryItems enriched with rgMbid + year from the matching MonitoredRelease. Client renders a cover-art grid with a live filter; clicking an album opens the AlbumModal (tracklist). Added Library to the nav. The album-only overview to see everything downloaded. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/app/_ui/contents-nav.tsx | 1 + web/src/app/api/library/route.test.ts | 29 +++++++++ web/src/app/api/library/route.ts | Bin 0 -> 1142 bytes web/src/app/design.css | 9 +++ web/src/app/library/library-client.tsx | 79 +++++++++++++++++++++++++ web/src/app/library/page.tsx | 5 ++ 6 files changed, 123 insertions(+) create mode 100644 web/src/app/api/library/route.test.ts create mode 100644 web/src/app/api/library/route.ts create mode 100644 web/src/app/library/library-client.tsx create mode 100644 web/src/app/library/page.tsx diff --git a/web/src/app/_ui/contents-nav.tsx b/web/src/app/_ui/contents-nav.tsx index ebc6ab5..1a4b706 100644 --- a/web/src/app/_ui/contents-nav.tsx +++ b/web/src/app/_ui/contents-nav.tsx @@ -4,6 +4,7 @@ import { usePathname } from "next/navigation"; const LINKS: { href: string; label: string }[] = [ { href: "/", label: "The Floor" }, + { href: "/library", label: "Library" }, { href: "/artists", label: "Artists" }, { href: "/discover", label: "Discover" }, { href: "/wanted", label: "Wanted" }, diff --git a/web/src/app/api/library/route.test.ts b/web/src/app/api/library/route.test.ts new file mode 100644 index 0000000..f70f2a8 --- /dev/null +++ b/web/src/app/api/library/route.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { prisma } from "@/lib/db"; +import { GET } from "./route"; + +describe("library API", () => { + it("lists owned albums enriched with rgMbid + year from the matching release", async () => { + await prisma.monitoredRelease.create({ + data: { + artistMbid: "a1", artistName: "Radiohead", rgMbid: "rg-inrainbows", album: "In Rainbows", + primaryType: "Album", firstReleaseDate: "2007-10-10", monitored: true, state: "fulfilled", + }, + }); + await prisma.libraryItem.create({ + data: { artist: "Radiohead", album: "In Rainbows", path: "/m", source: "qobuz", format: "FLAC", qualityClass: 3 }, + }); + await prisma.libraryItem.create({ + data: { artist: "Unknown", album: "Bootleg", path: "/m", source: "scan", format: "MP3", qualityClass: 1 }, + }); + + const res = await GET(); + expect(res.status).toBe(200); + const { albums } = await res.json(); + expect(albums).toHaveLength(2); + const inr = albums.find((a: { album: string }) => a.album === "In Rainbows"); + expect(inr).toMatchObject({ artist: "Radiohead", rgMbid: "rg-inrainbows", year: "2007", format: "FLAC", qualityClass: 3 }); + const boot = albums.find((a: { album: string }) => a.album === "Bootleg"); + expect(boot).toMatchObject({ rgMbid: null, year: null }); // no matching release → no art/tracks + }); +}); diff --git a/web/src/app/api/library/route.ts b/web/src/app/api/library/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..7162466ec36b9d1f0b185375775547d78c954a4e GIT binary patch literal 1142 zcmZWo!H&}~5aryjc&a!gqNE%+b+r_hMQ8L2US2+o4@0_et zNKLBPa&)iU7H%s+vXa8ew;M9e4vU++Tf9Ll1x0QTsf8=HN^d;(!f53T$?_rQA$c~+ z%NY$pPFXh%!s!Io1c6>38&V|-p<%aXkHY+x#$VZiJc6E+ZC=M_Ch)QWHXeNnW(B{$$0cWW$eyiLAF-9TnODJ2N)_P z-Lx|{R0U^c`N_>NE~PETY;KoM&ROb<*3YN{46y7;!3>AqiXWnuGj6YulQ@pyLYqad z%Hg(FWL@v)t#HQ_o^s8Tdu(_86S`PXENqot9Zh+WE!&X93Cv&}@w|?r@tWfL1D;+V zBB*=CC%+=y(}^Z;zXc*K_gIKs(l3Y2YQ7{C7?!M+P!?Hyhdc_loSAs$v`Rx&Bwa%q z57O>([]); + const [q, setQ] = useState(""); + const [open, setOpen] = useState(null); + + useEffect(() => { + fetch("/api/library") + .then((r) => r.json()) + .then((d) => setAlbums(d.albums ?? [])); + }, []); + + const shown = useMemo(() => { + const needle = q.trim().toLowerCase(); + if (!needle) return albums; + return albums.filter((a) => `${a.artist} ${a.album}`.toLowerCase().includes(needle)); + }, [albums, q]); + + return ( +
+ + +
e.preventDefault()}> + + + {shown.length} of {albums.length} albums + +
+ + {albums.length === 0 ? ( +

Nothing pressed yet. Scan your library in Settings, or queue a release from The Floor.

+ ) : ( +
+ {shown.map((a) => ( + + ))} +
+ )} + + {open ? ( + setOpen(null)} + album={{ rgMbid: open.rgMbid, album: open.album, artist: open.artist, year: open.year, type: open.primaryType }} + status={In library · {open.format}} + /> + ) : null} +
+ ); +} diff --git a/web/src/app/library/page.tsx b/web/src/app/library/page.tsx new file mode 100644 index 0000000..0777fb0 --- /dev/null +++ b/web/src/app/library/page.tsx @@ -0,0 +1,5 @@ +import { LibraryClient } from "./library-client"; + +export default function LibraryPage() { + return ; +}