diff --git a/core/imports/file_integrity.py b/core/imports/file_integrity.py index a0ebb18a..615af097 100644 --- a/core/imports/file_integrity.py +++ b/core/imports/file_integrity.py @@ -183,11 +183,29 @@ def check_audio_integrity( checks["actual_length_s"] = actual_length_s if actual_length_s <= 0: + # Length 0 is NOT proof of corruption here: the file already passed the + # size gate, was identified as a real audio format, and has a valid + # info block. A genuinely empty/truncated/stub file fails one of those + # earlier checks instead. The real cause of a clean-but-zero-length + # parse is "length unknown" — fragmented / streamed FLAC carries + # total_samples=0 in its STREAMINFO even though every audio frame is + # present and the file plays fine. HiFi is the common trigger: it + # assembles FLAC from HLS segments and demuxes with `ffmpeg -c copy`, + # which preserves total_samples=0, so mutagen computes length 0 and the + # file was wrongly quarantined (#756). Treat it as unknown length: + # accept the file and skip the duration cross-check we can't perform + # without a length. mutagen never decoded/validated frame data anyway, + # so accepting here doesn't weaken real corruption detection. + logger.warning( + "[Integrity] %s parsed cleanly (%d bytes, format=%s) but reports " + "length 0 — treating as unknown length (likely streamed/fragmented " + "FLAC), not rejecting", + os.path.basename(file_path), size, type(audio).__name__, + ) return IntegrityResult( - ok=False, - reason="Mutagen reports zero-length audio — file has no playable " - "audio data", - checks={**checks, "mutagen_parse": "zero_length"}, + ok=True, + checks={**checks, "mutagen_parse": "zero_length_unknown", + "length_check": "skipped_unknown_length"}, ) # --- Check 3: duration agreement (optional) --- diff --git a/tests/imports/test_file_integrity.py b/tests/imports/test_file_integrity.py index 0d56b531..21a2f810 100644 --- a/tests/imports/test_file_integrity.py +++ b/tests/imports/test_file_integrity.py @@ -124,6 +124,52 @@ def test_accepts_valid_wav_with_no_expected_duration(tmp_path: Path) -> None: assert result.checks["length_check"] == "skipped" +# --------------------------------------------------------------------------- +# Zero-length-but-valid parse (#756): streamed / fragmented FLAC +# --------------------------------------------------------------------------- + + +def test_accepts_zero_length_when_parse_is_otherwise_valid(tmp_path: Path, monkeypatch) -> None: + """#756: HiFi assembles FLAC from HLS segments and demuxes with + `ffmpeg -c copy`, leaving STREAMINFO total_samples=0. Mutagen then + reports length 0 even though every audio frame is present and the file + plays fine. Such a file — large, identifiable format, valid info block — + must be ACCEPTED (unknown length), not quarantined as 'zero-length'.""" + import mutagen + + f = tmp_path / "streamed.flac" + f.write_bytes(b"\x00" * (50 * 1024)) # clears size gate; bytes irrelevant (mutagen mocked) + + # Valid parse, valid info block, but length 0 — the streamed-FLAC signature. + fake_audio = SimpleNamespace(info=SimpleNamespace(length=0)) + monkeypatch.setattr(mutagen, "File", lambda *a, **k: fake_audio) + + # Even with an expected duration present, it must accept (can't compare to + # an unknown length) rather than reject. + result = file_integrity.check_audio_integrity(str(f), expected_duration_ms=200_000) + + assert result.ok is True + assert result.checks["mutagen_parse"] == "zero_length_unknown" + assert result.checks["length_check"] == "skipped_unknown_length" + + +def test_zero_length_still_rejects_when_too_small(tmp_path: Path, monkeypatch) -> None: + """A genuinely empty/stub file with length 0 must still fail — the size + gate fires before parse, so the relaxation can't let real stubs through.""" + import mutagen + + f = tmp_path / "stub.flac" + f.write_bytes(b"\x00" * 200) # below the 10KB size gate + + fake_audio = SimpleNamespace(info=SimpleNamespace(length=0)) + monkeypatch.setattr(mutagen, "File", lambda *a, **k: fake_audio) + + result = file_integrity.check_audio_integrity(str(f)) + + assert result.ok is False + assert "too small" in result.reason.lower() + + # --------------------------------------------------------------------------- # Duration agreement check # ---------------------------------------------------------------------------