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:
parent
b85e9b40d9
commit
319b6483a2
2 changed files with 80 additions and 4 deletions
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue