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>
This commit is contained in:
Jonathan
2026-07-14 21:28:54 +02:00
parent 7719b2a0d8
commit 8c8a34e320
9 changed files with 160 additions and 6 deletions
+18 -2
View File
@@ -146,6 +146,19 @@ def _is_upgrade_job(conn: psycopg.Connection, job_id: str) -> bool:
return bool(row and row[0])
def _is_force_job(conn: psycopg.Connection, job_id: str) -> bool:
"""A user-forced re-acquire (Library 'Replace / upgrade'): skip the already-in-library
dedupe so an owned album is re-downloaded. The import step still keeps the new copy only
if it's higher quality, so a forced upgrade can never downgrade what's on disk."""
with conn.cursor() as cur:
cur.execute(
'SELECT force FROM "Request" WHERE id = (SELECT "requestId" FROM "Job" WHERE id = %s)',
(job_id,),
)
row = cur.fetchone()
return bool(row and row[0])
def _already_in_library_at_cutoff(conn: psycopg.Connection, target: MBTarget, cutoff: int) -> bool:
with conn.cursor() as cur:
cur.execute(
@@ -237,8 +250,11 @@ def run_pipeline(
if resolved is not None:
target = resolved
# dedupe: a monitor-driven upgrade job proceeds unless the existing copy already meets
# the cutoff; a plain request short-circuits on any existing copy (slice-1 behavior).
if upgrade_cutoff is not None and _is_upgrade_job(conn, job_id):
# the cutoff; a plain request short-circuits on any existing copy (slice-1 behavior). A
# user-forced re-acquire skips the dedupe entirely (keep-if-better still guards the import).
if _is_force_job(conn, job_id):
_dedupe_hit = False
elif upgrade_cutoff is not None and _is_upgrade_job(conn, job_id):
_dedupe_hit = _already_in_library_at_cutoff(conn, target, upgrade_cutoff)
else:
_dedupe_hit = _already_in_library(conn, target)