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
|
_LENGTH_TOLERANCE_LONG_TRACK_S = 5.0
|
||||||
_LONG_TRACK_THRESHOLD_S = 600.0 # 10 minutes
|
_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
|
# Upper bound for the user-configurable override. Anything past 60s
|
||||||
# means the check is effectively off — cap defends against accidental
|
# means the check is effectively off — cap defends against accidental
|
||||||
# nonsense like 9999 making logs misleading. Users who genuinely want
|
# 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
|
if expected_length_s > _LONG_TRACK_THRESHOLD_S
|
||||||
else _DEFAULT_LENGTH_TOLERANCE_S
|
else _DEFAULT_LENGTH_TOLERANCE_S
|
||||||
)
|
)
|
||||||
|
user_pinned_tolerance = False
|
||||||
|
else:
|
||||||
|
user_pinned_tolerance = True
|
||||||
checks["length_tolerance_s"] = length_tolerance_s
|
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
|
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(
|
return IntegrityResult(
|
||||||
ok=False,
|
ok=False,
|
||||||
reason=f"Duration mismatch: file is {actual_length_s:.1f}s, "
|
reason=f"Duration mismatch: file is {actual_length_s:.1f}s, "
|
||||||
f"expected {expected_length_s:.1f}s "
|
f"expected {expected_length_s:.1f}s "
|
||||||
f"(drift {drift_s:.1f}s > tolerance {length_tolerance_s:.1f}s) — "
|
f"(drift {drift_s:.1f}s > tolerance {effective_tolerance_s:.1f}s) — "
|
||||||
"likely truncated download or wrong file matched",
|
+ ("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,
|
checks=checks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,60 @@ def test_rejects_truncated_file(tmp_path: Path) -> None:
|
||||||
assert result.checks["length_drift_s"] > 3.0
|
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:
|
def test_rejects_wrong_file_substituted(tmp_path: Path) -> None:
|
||||||
"""A 10-second clip masquerading as a 3-minute album track. slskd
|
"""A 10-second clip masquerading as a 3-minute album track. slskd
|
||||||
matched on a similar filename but the actual content is a snippet."""
|
matched on a similar filename but the actual content is a snippet."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue