feat(pipeline): reject downloads far shorter than the MB total duration
The verify step checked track COUNT but not playtime, so a truncated file or a short preview substituted for a real track could import as "complete". Add a total-duration check: sum the staged audio's playtime (mutagen) and fail the job when it falls below 85% of MusicBrainz's expected total (target.total_duration_s, already resolved). Generous tolerance so edition/encoding differences don't false-positive; bonus tracks (over-long) always pass. The duration reader is injected (measure_duration param, default the real mutagen-based reader) so the fail/pass paths are unit-tested; it returns None for the offline fakes (no real audio), skipping the check exactly like the staged-count guard. worker 233 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,39 @@ def test_dedupe_skips_already_in_library(conn):
|
||||
assert cur.fetchone()[0] == 1 # not duplicated
|
||||
|
||||
|
||||
class _DurationResolver:
|
||||
"""Resolver that stamps a known MB total duration on the target (the real MB path)."""
|
||||
def __init__(self, total_s, tracks=10):
|
||||
self._total, self._tracks = total_s, tracks
|
||||
def resolve(self, artist, album):
|
||||
from lyra_worker.types import MBTarget
|
||||
return MBTarget(artist=artist, album=album, track_count=self._tracks, total_duration_s=self._total)
|
||||
|
||||
|
||||
def test_rejects_a_download_far_shorter_than_the_mb_total(conn):
|
||||
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
|
||||
claim_next(conn)
|
||||
# MB says ~40 min; only ~5 min of audio staged (truncated/preview tracks) → reject.
|
||||
run_pipeline(conn, job_id, [FakeQobuz()], resolver=_DurationResolver(2400),
|
||||
measure_duration=lambda _s: 300.0, dest_root="/tmp/lib")
|
||||
state, _stage = _job_state(conn, job_id)
|
||||
assert state == "needs_attention"
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT error FROM "Job" WHERE id = %s', (job_id,))
|
||||
assert "too short" in cur.fetchone()[0]
|
||||
cur.execute('SELECT count(*) FROM "LibraryItem" WHERE album = %s', ("In Rainbows",))
|
||||
assert cur.fetchone()[0] == 0 # nothing imported
|
||||
|
||||
|
||||
def test_accepts_a_download_within_the_duration_tolerance(conn):
|
||||
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
|
||||
claim_next(conn)
|
||||
# ~39 of ~40 min → within the 85% tolerance → imports normally.
|
||||
run_pipeline(conn, job_id, [FakeQobuz()], resolver=_DurationResolver(2400),
|
||||
measure_duration=lambda _s: 2340.0, dest_root="/tmp/lib")
|
||||
assert _job_state(conn, job_id) == ("imported", "import")
|
||||
|
||||
|
||||
def test_force_reacquires_an_album_already_in_library(conn):
|
||||
# First acquisition puts the album in the library.
|
||||
job1 = insert_request(conn, artist="Radiohead", album="In Rainbows")
|
||||
|
||||
Reference in New Issue
Block a user