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:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LibraryItem" ADD COLUMN "rgMbid" TEXT;
|
||||||
@@ -82,6 +82,9 @@ model LibraryItem {
|
|||||||
format String
|
format String
|
||||||
qualityClass Int
|
qualityClass Int
|
||||||
importedAt DateTime @default(now())
|
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
|
// 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)
|
// 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
|
// instead of MusicBrainz's canonical listing. Empty for items imported before this existed
|
||||||
|
|||||||
@@ -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 { mkdtempSync, existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto";
|
|||||||
import Busboy from "busboy";
|
import Busboy from "busboy";
|
||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
import { isAuthed } from "@/lib/auth";
|
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
|
// 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
|
// 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 mkdir(dirname(finalDir), { recursive: true });
|
||||||
await rename(staging, finalDir);
|
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();
|
const firstExt = extname(audio[0]).toLowerCase();
|
||||||
await prisma.libraryItem.create({
|
await prisma.libraryItem.create({
|
||||||
data: {
|
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
|
// 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.
|
// can upgrade a hi-res file to class 3.
|
||||||
qualityClass: LOSSLESS_EXT.has(firstExt) ? 2 : 1,
|
qualityClass: LOSSLESS_EXT.has(firstExt) ? 2 : 1,
|
||||||
|
rgMbid,
|
||||||
trackNames: audio,
|
trackNames: audio,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user