fix(worker): reject a download with 0-byte/missing tracks (#16 partial)
Completeness only compared the adapter's self-reported track_count to expected, so a silently-skipped track (0-byte placeholder — see the Qobuz 24K Magic bug) imported as a "complete" album. Now the verify also counts the NON-EMPTY audio actually staged and fails "incomplete download" when fewer than expected are present (guarded with `staged and ...` so fake adapters that stage nothing keep using the count path). A real broken download now goes needs_attention (or falls through to another source) instead of importing a dead track. Full per-track duration matching against MB recording lengths (the rest of #16) remains a larger follow-up. worker 224 tests / 7-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -310,7 +310,11 @@ def run_pipeline(
|
|||||||
# 5. verify completeness on the staging copy, then promote + tag
|
# 5. verify completeness on the staging copy, then promote + tag
|
||||||
_set_state(conn, job_id, "tagging", "tag")
|
_set_state(conn, job_id, "tagging", "tag")
|
||||||
expected = target.track_count if target.track_count is not None else winner.track_count
|
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")
|
_fail(conn, job_id, "incomplete download")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user