feat(web): MB-resolve manual imports for instant cover art

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) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-14 20:05:59 +02:00
parent d52517ef39
commit 0147975b14
5 changed files with 18 additions and 1 deletions
+2 -1
View File
@@ -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";
+11
View File
@@ -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,
},
});
Binary file not shown.