feat(worker): defer Qobuz upgrades when it's gated instead of re-grabbing same quality
With qualityCutoff=3 (want hi-res), a Soulseek-grabbed CD copy stays below cutoff and is re-attempted for upgrade. Without a guard, when Qobuz is gated by pacing those attempts would (a) re-search every owned album each monitor cycle and (b) re-download a same-quality Soulseek copy that keep-if-better discards — pure churn. Add an upgrade guard: a >= CD-lossless copy can only be improved by hi-res Qobuz, so when Qobuz is gated, skip the job WITHOUT searching (pre-search skip); and as a safety net, skip the download when the best-ranked candidate can't beat the existing copy. Force jobs always proceed. Extracted the shared keep-existing path into a helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,10 @@ _DURATION_MIN_RATIO = 0.85
|
||||
# album returns ~200 Soulseek candidates), each a slow attempt that also leaves an abandoned slskd
|
||||
# transfer. The best candidates rank first, so 6 attempts is plenty.
|
||||
_MAX_DOWNLOAD_ATTEMPTS = 6
|
||||
# The best quality any non-Qobuz source offers (Soulseek FLAC = CD-lossless). A library copy at or
|
||||
# above this can only be improved by hi-res Qobuz, so when Qobuz is gated by pacing an upgrade of
|
||||
# such a copy is deferred without even searching.
|
||||
_CD_LOSSLESS_CLASS = 2
|
||||
|
||||
|
||||
def _measure_staged_duration_s(staging: str) -> float | None:
|
||||
@@ -266,6 +270,17 @@ def _library_quality(conn: psycopg.Connection, target: MBTarget) -> int | None:
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def _keep_existing_and_complete(conn: psycopg.Connection, job_id: str) -> None:
|
||||
"""Finish a job without importing — keep the existing library copy — and mark its request
|
||||
completed. Used when a (deferred) upgrade has nothing better to offer right now; the release
|
||||
stays below the cutoff and is re-attempted later."""
|
||||
_set_state(conn, job_id, "imported", "import")
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("UPDATE \"Request\" SET status = 'completed' WHERE id = %s",
|
||||
(_request_id(conn, job_id),))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _persist_candidates(conn: psycopg.Connection, job_id: str, candidates: list[Candidate]) -> None:
|
||||
with conn.cursor() as cur:
|
||||
for c in candidates:
|
||||
@@ -358,6 +373,16 @@ def run_pipeline(
|
||||
conn.commit()
|
||||
return
|
||||
|
||||
# Pre-search upgrade skip: a >= CD-lossless copy can only be improved by hi-res Qobuz. If Qobuz
|
||||
# is gated by pacing, don't even search (no other source can beat it) — defer to when Qobuz
|
||||
# frees up. This avoids re-searching every already-owned album (~2 min Soulseek search each) on
|
||||
# every monitor cycle. A user-forced re-acquire always proceeds.
|
||||
_existing_q = _library_quality(conn, target)
|
||||
if _existing_q is not None and _existing_q >= _CD_LOSSLESS_CLASS \
|
||||
and not _is_force_job(conn, job_id) and not gate_open(conn):
|
||||
_keep_existing_and_complete(conn, job_id)
|
||||
return
|
||||
|
||||
# 2. match — search all adapters CONCURRENTLY (Soulseek's search polls up to ~2 min; run it
|
||||
# alongside Qobuz's instant search so total match time = the slowest source, not their sum).
|
||||
# Only Qobuz touches streamrip's shared asyncio loop and there's a single Qobuz adapter, so no
|
||||
@@ -386,6 +411,17 @@ def run_pipeline(
|
||||
_fail(conn, job_id, "no candidate above confidence threshold")
|
||||
return
|
||||
|
||||
# Upgrade guard: for an album already in the library, skip the download when no currently-
|
||||
# available source beats the copy we have (ranked[0] is the highest-quality candidate). This
|
||||
# happens when Qobuz is gated by pacing and only a same-quality Soulseek copy is left — the
|
||||
# download would be discarded by keep-if-better, so defer it. The release stays below the cutoff
|
||||
# and is re-attempted when a better source frees up. A user-forced re-acquire always proceeds.
|
||||
existing_q = _library_quality(conn, target)
|
||||
if existing_q is not None and not _is_force_job(conn, job_id) \
|
||||
and quality_class(ranked[0].quality) <= existing_q:
|
||||
_keep_existing_and_complete(conn, job_id) # keep the existing copy; nothing better available
|
||||
return
|
||||
|
||||
# 4. download (fall-through) into an isolated per-job staging dir
|
||||
_set_state(conn, job_id, "downloading", "download")
|
||||
by_source = {a.name: a for a in adapters}
|
||||
@@ -455,12 +491,7 @@ def run_pipeline(
|
||||
print(f"pipeline: tagging failed for job {job_id}: {e}", flush=True)
|
||||
_import(conn, job_id, target, winner, final)
|
||||
else:
|
||||
# an existing copy is already at >= this quality — keep it untouched, just complete the request
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE \"Request\" SET status = 'completed' WHERE id = %s",
|
||||
(_request_id(conn, job_id),),
|
||||
)
|
||||
conn.commit()
|
||||
# an existing copy is already at >= this quality — keep it untouched, just complete
|
||||
_keep_existing_and_complete(conn, job_id)
|
||||
finally:
|
||||
shutil.rmtree(staging, ignore_errors=True) # a partial/failed download never persists
|
||||
|
||||
Reference in New Issue
Block a user