feat(worker): fall through to the next source on an incomplete download
Completeness/duration verification was applied only to the first candidate that downloaded successfully; if that copy was incomplete or too short the whole job failed, even when a later-ranked source had a complete copy (e.g. John Mayer 'The Search for Everything' — Qobuz reliably fails one hi-res track, so it never fell back to the deluxe edition or Soulseek). Extract the checks into a pure _download_problem() and run it INSIDE the download loop: a candidate that downloads but fails verification now falls through to the next-ranked source, and the job only fails (needs_attention) when every candidate is incomplete — reporting the last shortfall reason. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""When the top-ranked source downloads but is incomplete (e.g. Qobuz consistently fails one
|
||||
track), the pipeline must fall through to the next-ranked source instead of failing the whole
|
||||
job — only giving up (needs_attention) when every candidate is incomplete."""
|
||||
import os
|
||||
|
||||
from lyra_worker.claim import claim_next
|
||||
from lyra_worker.pipeline import run_pipeline
|
||||
from lyra_worker.types import Candidate, DownloadResult, Quality
|
||||
from tests.conftest import insert_request
|
||||
|
||||
_HIRES = Quality(fmt="FLAC", lossless=True, bit_depth=24, sample_rate=96000)
|
||||
_CD = Quality(fmt="FLAC", lossless=True, bit_depth=16, sample_rate=44100)
|
||||
|
||||
|
||||
def _stage(dest, n):
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
for i in range(n):
|
||||
with open(os.path.join(dest, f"{i + 1:02d}.flac"), "wb") as fh:
|
||||
fh.write(b"audio")
|
||||
|
||||
|
||||
class _Qobuz:
|
||||
"""Higher-quality (ranks first) Qobuz that promises 3 tracks but delivers `deliver`."""
|
||||
name = "qobuz"
|
||||
tier = 0
|
||||
|
||||
def __init__(self, deliver):
|
||||
self._deliver = deliver
|
||||
|
||||
def health(self):
|
||||
return True
|
||||
|
||||
def search(self, target):
|
||||
return [Candidate(source="qobuz", source_ref="q", matched_artist=target.artist,
|
||||
matched_album=target.album, quality=_HIRES, track_count=3, source_tier=0)]
|
||||
|
||||
def download(self, candidate, dest, on_progress):
|
||||
_stage(dest, self._deliver)
|
||||
return DownloadResult(ok=True, path=dest, track_count=self._deliver)
|
||||
|
||||
|
||||
class _Soulseek:
|
||||
"""Lower-quality (ranks second) Soulseek that promises 3 tracks and delivers `deliver`."""
|
||||
name = "soulseek"
|
||||
tier = 1
|
||||
|
||||
def __init__(self, deliver):
|
||||
self._deliver = deliver
|
||||
|
||||
def health(self):
|
||||
return True
|
||||
|
||||
def search(self, target):
|
||||
return [Candidate(source="soulseek", source_ref="s", matched_artist=target.artist,
|
||||
matched_album=target.album, quality=_CD, track_count=3, source_tier=1)]
|
||||
|
||||
def download(self, candidate, dest, on_progress):
|
||||
_stage(dest, self._deliver)
|
||||
return DownloadResult(ok=True, path=dest, track_count=self._deliver)
|
||||
|
||||
|
||||
def _state_error(conn, job_id):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT state, error FROM "Job" WHERE id = %s', (job_id,))
|
||||
return cur.fetchone()
|
||||
|
||||
|
||||
def _chosen_source(conn, job_id):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT source FROM "Candidate" WHERE "jobId" = %s AND chosen = true', (job_id,))
|
||||
row = cur.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def test_incomplete_top_source_falls_through_to_complete_next(conn, tmp_path):
|
||||
# Qobuz (ranks first on quality) delivers only 2 of its promised 3 tracks -> incomplete.
|
||||
# The pipeline must try the next source (Soulseek, complete) rather than failing the job.
|
||||
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
|
||||
claim_next(conn)
|
||||
run_pipeline(conn, job_id, [_Qobuz(deliver=2), _Soulseek(deliver=3)], dest_root=str(tmp_path))
|
||||
assert _state_error(conn, job_id) == ("imported", None)
|
||||
assert _chosen_source(conn, job_id) == "soulseek"
|
||||
|
||||
|
||||
def test_all_sources_incomplete_still_needs_attention(conn, tmp_path):
|
||||
# Every candidate is incomplete -> no fallback succeeds -> needs_attention (incomplete).
|
||||
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
|
||||
claim_next(conn)
|
||||
run_pipeline(conn, job_id, [_Qobuz(deliver=2), _Soulseek(deliver=1)], dest_root=str(tmp_path))
|
||||
assert _state_error(conn, job_id) == ("needs_attention", "incomplete download")
|
||||
assert not (tmp_path / "Radiohead" / "In Rainbows").exists() # nothing imported
|
||||
Reference in New Issue
Block a user