From 0147975b1415a6e33fd6a132e72faa3eb7f7c1b1 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 14 Jul 2026 20:05:59 +0200 Subject: [PATCH] feat(web): MB-resolve manual imports for instant cover art MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A manually-imported album had no release-group MBID, so it showed the ◉ placeholder until a Scan matched it. Now the import resolves the release group (searchReleaseGroup, via the shared MB cache) and stores it on the new LibraryItem.rgMbid column (migration add_library_rg_mbid). The library route prefers LibraryItem.rgMbid, falling back to the MonitoredRelease join for scanned items — so a manual album gets cover art immediately. Best-effort: a no-match / MB outage just leaves rgMbid null. web 164 tests, tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migration.sql | 2 ++ web/prisma/schema.prisma | 3 +++ web/src/app/api/library/import/route.test.ts | 3 ++- web/src/app/api/library/import/route.ts | 11 +++++++++++ web/src/app/api/library/route.ts | Bin 1239 -> 1252 bytes 5 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 web/prisma/migrations/20260714180402_add_library_rg_mbid/migration.sql diff --git a/web/prisma/migrations/20260714180402_add_library_rg_mbid/migration.sql b/web/prisma/migrations/20260714180402_add_library_rg_mbid/migration.sql new file mode 100644 index 0000000..4243834 --- /dev/null +++ b/web/prisma/migrations/20260714180402_add_library_rg_mbid/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "LibraryItem" ADD COLUMN "rgMbid" TEXT; diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index e17461a..5c1c5e9 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -82,6 +82,9 @@ model LibraryItem { format String qualityClass Int importedAt DateTime @default(now()) + // MusicBrainz release-group MBID when known (resolved at manual import) — gives the album + // cover art without depending on a matching MonitoredRelease row. + rgMbid String? // The album's on-disk audio filenames ("## Title.ext"), captured at import + scan, so the // Library modal can show the REAL tracks on disk (revealing incomplete/edition mismatches) // instead of MusicBrainz's canonical listing. Empty for items imported before this existed diff --git a/web/src/app/api/library/import/route.test.ts b/web/src/app/api/library/import/route.test.ts index a54a8db..271d900 100644 --- a/web/src/app/api/library/import/route.test.ts +++ b/web/src/app/api/library/import/route.test.ts @@ -1,4 +1,5 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +vi.mock("@/lib/musicbrainz", () => ({ searchReleaseGroup: vi.fn(async () => null) })); import { mkdtempSync, existsSync, readFileSync, readdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; diff --git a/web/src/app/api/library/import/route.ts b/web/src/app/api/library/import/route.ts index 67656f4..55df0c4 100644 --- a/web/src/app/api/library/import/route.ts +++ b/web/src/app/api/library/import/route.ts @@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto"; import Busboy from "busboy"; import { prisma } from "@/lib/db"; import { isAuthed } from "@/lib/auth"; +import { searchReleaseGroup } from "@/lib/musicbrainz"; // Manually import an album (a ripped CD / files already on the PC): upload the audio (+ an // optional cover) with artist/album/year, and Lyra writes it into the library. The web @@ -104,6 +105,15 @@ export async function POST(request: Request) { await mkdir(dirname(finalDir), { recursive: true }); await rename(staging, finalDir); + // Resolve the release group so the album gets cover art immediately (best-effort — a + // no-match just leaves rgMbid null; a later Scan can still match it). Uses the shared MB cache. + let rgMbid: string | null = null; + try { + rgMbid = (await searchReleaseGroup(artist, album))?.rgMbid ?? null; + } catch { + /* MB unavailable — carry on without cover art */ + } + const firstExt = extname(audio[0]).toLowerCase(); await prisma.libraryItem.create({ data: { @@ -115,6 +125,7 @@ export async function POST(request: Request) { // rough class from the extension (2 = CD-lossless, 1 = lossy); a later scan re-probes and // can upgrade a hi-res file to class 3. qualityClass: LOSSLESS_EXT.has(firstExt) ? 2 : 1, + rgMbid, trackNames: audio, }, }); diff --git a/web/src/app/api/library/route.ts b/web/src/app/api/library/route.ts index b8bcf4373a3b789212abfd5e6d8509c821ebf6ea..fc8be4bfd998aefe88bd77ba3fd425d85a1fd42d 100644 GIT binary patch delta 24 gcmcc4`Gj-B7iONEOueFX-=xeG1$+C+pP8Qk0DI#J