diff --git a/worker/lyra_worker/pipeline.py b/worker/lyra_worker/pipeline.py index 5e0a0dd..8bc4ea6 100644 --- a/worker/lyra_worker/pipeline.py +++ b/worker/lyra_worker/pipeline.py @@ -310,7 +310,11 @@ def run_pipeline( # 5. verify completeness on the staging copy, then promote + tag _set_state(conn, job_id, "tagging", "tag") expected = target.track_count if target.track_count is not None else winner.track_count - if result.track_count < expected: + # Also count the NON-EMPTY audio actually on disk: a silently-skipped track leaves a + # 0-byte placeholder that the adapter still counts, so trust the files when any are + # staged. (`staged and ...` keeps fake adapters that stage nothing on the count path.) + staged = _count_staged_audio(staging) + if result.track_count < expected or (staged and staged < expected): _fail(conn, job_id, "incomplete download") return diff --git a/worker/tests/test_incomplete_download.py b/worker/tests/test_incomplete_download.py new file mode 100644 index 0000000..8ae56fa --- /dev/null +++ b/worker/tests/test_incomplete_download.py @@ -0,0 +1,39 @@ +"""A track that silently fails to download leaves a 0-byte placeholder but the adapter still +reports the full count; the pipeline must reject it (incomplete) rather than import a broken +album. (Motivated by Bruno Mars "24K Magic" track 1 downloading as 0 bytes.)""" +import os + +from lyra_worker.adapters.fakes import FakeQobuz +from lyra_worker.claim import claim_next +from lyra_worker.pipeline import run_pipeline +from lyra_worker.types import DownloadResult +from tests.conftest import insert_request + + +class _ZeroByteTrackFake(FakeQobuz): + """Stages (n-1) real tracks + one 0-byte placeholder but reports the full count n.""" + def download(self, candidate, dest, on_progress): + os.makedirs(dest, exist_ok=True) + n = candidate.track_count + for i in range(n - 1): + with open(os.path.join(dest, f"{i + 1:02d} Track.flac"), "wb") as fh: + fh.write(b"audio") + with open(os.path.join(dest, f"{n:02d} Dead.flac"), "wb") as fh: + fh.write(b"") # the silently-skipped track + return DownloadResult(ok=True, path=dest, track_count=n) + + +def test_zero_byte_track_fails_completeness(conn): + job_id = insert_request(conn, artist="Radiohead", album="In Rainbows") + claim_next(conn) + run_pipeline(conn, job_id, [_ZeroByteTrackFake()], dest_root="/tmp/lib-zbt") + + with conn.cursor() as cur: + cur.execute('SELECT state, error FROM "Job" WHERE id = %s', (job_id,)) + state, error = cur.fetchone() + assert state == "needs_attention" + assert error == "incomplete download" + # nothing was imported + with conn.cursor() as cur: + cur.execute('SELECT count(*) FROM "LibraryItem" WHERE artist = %s', ("Radiohead",)) + assert cur.fetchone()[0] == 0