From 319b6483a2b938f5f68fd5a4149463b47420c2a3 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sat, 27 Jun 2026 15:25:13 -0700 Subject: [PATCH] integrity: don't quarantine longer masters/versions as 'truncated' (#937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/imports/file_integrity.py | 30 +++++++++++++--- tests/imports/test_file_integrity.py | 54 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/core/imports/file_integrity.py b/core/imports/file_integrity.py index 4891cfd1..46542e53 100644 --- a/core/imports/file_integrity.py +++ b/core/imports/file_integrity.py @@ -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, ) diff --git a/tests/imports/test_file_integrity.py b/tests/imports/test_file_integrity.py index 21a2f810..cb75e3aa 100644 --- a/tests/imports/test_file_integrity.py +++ b/tests/imports/test_file_integrity.py @@ -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."""