b0eec41823
The completeness + duration guards used MusicBrainz's target.track_count / total_duration_s, but MB picks an arbitrary releases[0] that is often a deluxe/expanded edition with more tracks (and runtime) than the standard album a source legitimately delivers. So complete standard-edition downloads were falsely rejected — The Script 'No Sound Without Silence' (11 vs MB 12) as 'incomplete download', Skillet 'Unleashed' (12 vs MB's 20-track deluxe) as 'too short'. Now completeness is judged against the chosen source's own track count (winner.track_count): a truncated download (fewer files than the source promised) still fails, and the duration expectation is scaled to the delivered edition's size. A separate 'implausibly small vs MB' guard (source has <= half MB's tracks) still rejects a lone-track 'full album' video. Genuine partials (e.g. John Mayer, a Qobuz track failing mid-download) still correctly fail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.7 KiB
Python
102 lines
4.7 KiB
Python
"""MusicBrainz picks an arbitrary releases[0] that is often a deluxe/expanded edition with more
|
|
tracks (and a longer runtime) than the standard album a source legitimately delivers. Completeness
|
|
and the duration guard must be judged against what the CHOSEN SOURCE offered, not MB's canonical
|
|
edition — otherwise a complete standard-edition download is falsely rejected as 'incomplete'
|
|
(The Script "No Sound Without Silence": 11 delivered vs MB 12) or 'too short' (Skillet "Unleashed":
|
|
12 delivered ~40 min vs MB's 20-track deluxe ~73 min). A genuinely short download must still fail.
|
|
"""
|
|
import os
|
|
from dataclasses import replace
|
|
|
|
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, MBTarget
|
|
from tests.conftest import insert_request
|
|
|
|
|
|
class _StandardEditionQobuz(FakeQobuz):
|
|
"""Qobuz-like source offering a fixed-size *standard* edition (source_tracks), independent of
|
|
MB's possibly-larger deluxe count; stages that many real (non-empty) files and reports them."""
|
|
def __init__(self, source_tracks):
|
|
super().__init__()
|
|
self._n = source_tracks
|
|
|
|
def search(self, target):
|
|
return super().search(replace(target, track_count=self._n))
|
|
|
|
def download(self, candidate, dest, on_progress):
|
|
os.makedirs(dest, exist_ok=True)
|
|
for i in range(candidate.track_count):
|
|
with open(os.path.join(dest, f"{i + 1:02d} Track.flac"), "wb") as fh:
|
|
fh.write(b"audio")
|
|
on_progress(1.0)
|
|
return DownloadResult(ok=True, path=dest, track_count=candidate.track_count)
|
|
|
|
|
|
class _PartialQobuz(FakeQobuz):
|
|
"""Promises `promised` tracks but one fails mid-download: stages and reports `promised - 1`
|
|
(the real John Mayer "Roll it on Home" IncompleteRead case)."""
|
|
def __init__(self, promised):
|
|
super().__init__()
|
|
self._n = promised
|
|
|
|
def search(self, target):
|
|
return super().search(replace(target, track_count=self._n))
|
|
|
|
def download(self, candidate, dest, on_progress):
|
|
os.makedirs(dest, exist_ok=True)
|
|
delivered = candidate.track_count - 1
|
|
for i in range(delivered):
|
|
with open(os.path.join(dest, f"{i + 1:02d} Track.flac"), "wb") as fh:
|
|
fh.write(b"audio")
|
|
return DownloadResult(ok=True, path=dest, track_count=delivered)
|
|
|
|
|
|
class _DeluxeResolver:
|
|
"""Resolver whose canonical release is a larger (deluxe) edition than the source delivers."""
|
|
def __init__(self, mb_tracks, mb_total_s):
|
|
self._t, self._s = mb_tracks, mb_total_s
|
|
|
|
def resolve(self, artist, album):
|
|
return MBTarget(artist=artist, album=album, track_count=self._t, total_duration_s=self._s)
|
|
|
|
|
|
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 test_standard_edition_not_rejected_as_incomplete(conn):
|
|
# The Script: source delivers the 11-track standard edition; MB's canonical release is 12.
|
|
job_id = insert_request(conn, artist="The Script", album="No Sound Without Silence")
|
|
claim_next(conn)
|
|
run_pipeline(conn, job_id, [_StandardEditionQobuz(source_tracks=11)],
|
|
resolver=_DeluxeResolver(mb_tracks=12, mb_total_s=2943),
|
|
measure_duration=lambda _s: 2700.0, dest_root="/tmp/lib-se")
|
|
assert _state_error(conn, job_id) == ("imported", None)
|
|
|
|
|
|
def test_smaller_edition_not_rejected_as_too_short(conn):
|
|
# Skillet: source delivers the 12-track standard (~40 min); MB's release is a 20-track deluxe
|
|
# (~73 min). The unscaled duration guard falsely rejects; scaling to the delivered edition
|
|
# (12/20) must let it pass.
|
|
job_id = insert_request(conn, artist="Skillet", album="Unleashed")
|
|
claim_next(conn)
|
|
run_pipeline(conn, job_id, [_StandardEditionQobuz(source_tracks=12)],
|
|
resolver=_DeluxeResolver(mb_tracks=20, mb_total_s=4380),
|
|
measure_duration=lambda _s: 2400.0, dest_root="/tmp/lib-se")
|
|
assert _state_error(conn, job_id) == ("imported", None)
|
|
|
|
|
|
def test_genuinely_incomplete_still_rejected(conn):
|
|
# John Mayer: the source promised 12 tracks but one failed mid-download → only 11 land.
|
|
# This is a real partial and must still be rejected (the fix must not swallow it).
|
|
job_id = insert_request(conn, artist="John Mayer", album="The Search for Everything")
|
|
claim_next(conn)
|
|
run_pipeline(conn, job_id, [_PartialQobuz(promised=12)],
|
|
resolver=_DeluxeResolver(mb_tracks=12, mb_total_s=2580),
|
|
measure_duration=lambda _s: 2400.0, dest_root="/tmp/lib-se")
|
|
assert _state_error(conn, job_id) == ("needs_attention", "incomplete download")
|