Files
Lyra/web/src/app/api/library/[id]/upgrade/route.ts
T
Jonathan 8c8a34e320 feat(library): replace/upgrade an owned album on demand
Add Request.force (migration add_request_force): the pipeline's intake now
skips the "already in library" dedupe for a forced job, so an owned album is
re-downloaded. The import step still keeps the new copy only when it's higher
quality, so a forced upgrade can never downgrade what's on disk.

- Worker: _is_force_job → dedupe bypassed when Request.force.
- POST /api/library/[id]/upgrade finds the matching MonitoredRelease and
  enqueues a force request (guards against stacking on an in-flight job).
- Library route exposes monitoredReleaseId; the album modal shows a
  "Replace / upgrade" button when a release exists.

Worker + web tests added (pipeline force re-acquire; upgrade route).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:28:54 +02:00

42 lines
1.9 KiB
TypeScript

import { prisma } from "@/lib/db";
// POST /api/library/[id]/upgrade — force a re-acquire of an owned album: enqueue a job that
// skips the "already in library" dedupe (Request.force). The pipeline downloads the best
// available copy and the import step keeps it only if it's higher quality, so this can upgrade
// but never downgrades. Requires a matching MonitoredRelease (scanned/monitored albums have
// one) to carry the acquisition context.
export async function POST(_request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const item = await prisma.libraryItem.findUnique({ where: { id } });
if (!item) return Response.json({ error: "not found" }, { status: 404 });
// Prefer the release-group match; fall back to an artist+album match.
const release =
(item.rgMbid ? await prisma.monitoredRelease.findUnique({ where: { rgMbid: item.rgMbid } }) : null) ??
(await prisma.monitoredRelease.findFirst({ where: { artistName: item.artist, album: item.album } }));
if (!release) {
return Response.json(
{ error: "no release info for this album — follow the artist or re-scan to enable upgrades" },
{ status: 400 },
);
}
// Don't stack a second job on a release that's already in flight.
const activeJob = await prisma.job.findFirst({
where: { request: { monitoredReleaseId: release.id }, state: { notIn: ["imported", "needs_attention"] } },
});
if (activeJob) return Response.json({ enqueued: false }, { status: 202 });
await prisma.request.create({
data: {
artist: release.artistName,
album: release.album,
monitoredReleaseId: release.id,
force: true,
job: { create: {} },
},
});
await prisma.monitoredRelease.update({ where: { id: release.id }, data: { lastSearchedAt: new Date() } });
return Response.json({ enqueued: true }, { status: 202 });
}