integrity: don't quarantine longer masters/versions as 'truncated' (#937)

the duration-agreement check used abs() drift, so a file running LONGER than the metadata
(a remaster with a longer outro, an extended cut) was rejected the same as a truncated one —
e.g. A-Ha 'Take on Me' remaster at 228.5s vs 225.0s expected, quarantined for +3.5s.

but a longer file is the OPPOSITE of truncated. make the auto tolerance asymmetric: keep the
tight 3s/5s bound for SHORTER files (the truncation case the check exists for), allow up to
15s in the LONGER direction for version/master differences. a wrong song still trips it (off
by far more than 15s), and a user-pinned tolerance is honoured symmetrically. direction-aware
rejection message too. 4 new tests; 274 integrity/import tests green.
This commit is contained in:
BoulderBadgeDad 2026-06-27 15:25:13 -07:00
parent b85e9b40d9
commit 319b6483a2
2 changed files with 80 additions and 4 deletions

View file

@ -52,6 +52,14 @@ _DEFAULT_LENGTH_TOLERANCE_S = 3.0
_LENGTH_TOLERANCE_LONG_TRACK_S = 5.0
_LONG_TRACK_THRESHOLD_S = 600.0 # 10 minutes
# A file that runs LONGER than the expected metadata is the opposite of a truncated
# download — it's almost always a different master/version (a remaster with a longer
# outro, an extended fade, an album cut vs the radio edit). The duration check exists to
# catch TRUNCATION (short files) and wildly-wrong matches, so on the auto default we allow
# more drift in the longer direction and keep the tight bound for short files. A wrong-song
# match still trips this — it's usually off by far more than 15s. (#937)
_LONGER_VERSION_TOLERANCE_S = 15.0
# Upper bound for the user-configurable override. Anything past 60s
# means the check is effectively off — cap defends against accidental
# nonsense like 9999 making logs misleading. Users who genuinely want
@ -242,18 +250,32 @@ def check_audio_integrity(
if expected_length_s > _LONG_TRACK_THRESHOLD_S
else _DEFAULT_LENGTH_TOLERANCE_S
)
user_pinned_tolerance = False
else:
user_pinned_tolerance = True
checks["length_tolerance_s"] = length_tolerance_s
drift_s = abs(actual_length_s - expected_length_s)
# Positive drift = the file runs LONGER than expected (not truncation). On the auto
# default, give the longer direction more room so legit longer masters/versions aren't
# quarantined (#937); a user-pinned tolerance is honoured symmetrically.
signed_drift_s = actual_length_s - expected_length_s
drift_s = abs(signed_drift_s)
checks["length_drift_s"] = drift_s
effective_tolerance_s = length_tolerance_s
if signed_drift_s > 0 and not user_pinned_tolerance:
effective_tolerance_s = max(length_tolerance_s, _LONGER_VERSION_TOLERANCE_S)
checks["effective_tolerance_s"] = effective_tolerance_s
if drift_s > length_tolerance_s:
if drift_s > effective_tolerance_s:
runs_long = signed_drift_s > 0
return IntegrityResult(
ok=False,
reason=f"Duration mismatch: file is {actual_length_s:.1f}s, "
f"expected {expected_length_s:.1f}s "
f"(drift {drift_s:.1f}s > tolerance {length_tolerance_s:.1f}s) — "
"likely truncated download or wrong file matched",
f"(drift {drift_s:.1f}s > tolerance {effective_tolerance_s:.1f}s) — "
+ ("runs longer than expected — likely a different version/master or wrong file"
if runs_long
else "likely truncated download or wrong file matched"),
checks=checks,
)

View file

@ -201,6 +201,60 @@ def test_rejects_truncated_file(tmp_path: Path) -> None:
assert result.checks["length_drift_s"] > 3.0
# ── #937: a file that runs LONGER than expected is a version/master difference, not
# truncation — it gets more leeway, while SHORTER files stay tight. ──
def test_accepts_longer_master_beyond_short_tolerance(tmp_path: Path) -> None:
"""The reported case (A-Ha remaster): file runs ~3.5s LONGER than the metadata.
Past the 3s short-tolerance but a remaster, not a bad download must pass."""
f = tmp_path / "remaster.wav"
_write_minimal_wav(f, duration_s=9.0) # 9.0s file vs 5.5s expected → +3.5s longer
result = file_integrity.check_audio_integrity(str(f), expected_duration_ms=5500)
assert result.ok is True
assert result.checks["length_check"] == "passed"
assert result.checks["effective_tolerance_s"] == pytest.approx(15.0)
def test_shorter_file_still_tight_after_longer_loosening(tmp_path: Path) -> None:
"""Loosening the LONGER direction must not loosen truncation detection — a file
3.5s SHORTER than expected is still rejected at the 3s tolerance."""
f = tmp_path / "short.wav"
_write_minimal_wav(f, duration_s=5.5) # 5.5s vs 9.0s expected → -3.5s shorter
result = file_integrity.check_audio_integrity(str(f), expected_duration_ms=9000)
assert result.ok is False
assert "truncated" in result.reason.lower()
assert result.checks["effective_tolerance_s"] == pytest.approx(3.0)
def test_wildly_longer_file_still_rejected(tmp_path: Path) -> None:
"""A different/wrong song that happens to run long is still caught — +25s blows
past even the 15s longer-tolerance."""
f = tmp_path / "wronglong.wav"
_write_minimal_wav(f, duration_s=30.0) # 30s vs 5s expected → +25s
result = file_integrity.check_audio_integrity(str(f), expected_duration_ms=5000)
assert result.ok is False
assert "longer than expected" in result.reason.lower()
def test_user_pinned_tolerance_is_symmetric(tmp_path: Path) -> None:
"""An explicit user tolerance is honoured in BOTH directions — the longer-direction
loosening only applies to the auto default, not a value the user pinned."""
f = tmp_path / "long.wav"
_write_minimal_wav(f, duration_s=9.0) # +4s longer
result = file_integrity.check_audio_integrity(
str(f), expected_duration_ms=5000, length_tolerance_s=2.0)
assert result.ok is False
assert result.checks["effective_tolerance_s"] == pytest.approx(2.0)
def test_rejects_wrong_file_substituted(tmp_path: Path) -> None:
"""A 10-second clip masquerading as a 3-minute album track. slskd
matched on a similar filename but the actual content is a snippet."""