From aa283c50c1b71655a7712f146531027e296626a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 6 Jul 2026 07:33:16 +0900 Subject: [PATCH] fix: fall back to format duration when audio stream reports zero probe_media mis-parsed valid media whose audio stream carries "duration": "0.000000" (or "0") while the true duration lives in the format section. _first_float stopped at the first parseable value (0.0) and never consulted the format fallback, so the file wrongly raised "has no usable duration". Parse the stream duration first and, when it is non-positive (unusable), fall back to the format duration before deciding the file has no duration. Adds a regression test reproducing the real ffprobe payload. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CJRVbDrp1vGYkJgNHMGPpG --- media_shrinker.py | 8 +++++--- tests/test_media_shrinker.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/media_shrinker.py b/media_shrinker.py index d72a8d5..d0c333a 100644 --- a/media_shrinker.py +++ b/media_shrinker.py @@ -1507,9 +1507,11 @@ def _parse_probe_payload( raise MediaShrinkerError(f"{source_path} has no audio stream") format_section = payload.get("format", {}) - duration = _first_float( - audio_stream.get("duration"), format_section.get("duration") - ) + # Prefer the stream duration, but a stream-level "0"/"0.000000" (reported by + # some containers) is unusable and must fall back to the format duration. + duration = _first_float(audio_stream.get("duration")) + if duration <= 0: + duration = _first_float(format_section.get("duration")) if duration <= 0: raise MediaShrinkerError(f"{source_path} has no usable duration") diff --git a/tests/test_media_shrinker.py b/tests/test_media_shrinker.py index 0ab2397..7c3dc2c 100644 --- a/tests/test_media_shrinker.py +++ b/tests/test_media_shrinker.py @@ -238,6 +238,28 @@ def test_parse_probe_payload_uses_known_source_size_without_stat(self) -> None: self.assertEqual(probe.size_bytes, 123) + def test_parse_probe_payload_falls_back_to_format_duration_when_stream_is_zero( + self, + ) -> None: + # Real ffprobe output: some containers report a stream-level + # "duration": "0.000000" while the true duration lives in the format + # section. The stream's unusable zero must not shadow the format value. + payload = { + "streams": [ + { + "codec_type": "audio", + "codec_name": "aac", + "duration": "0.000000", + "bit_rate": "128000", + } + ], + "format": {"duration": "123.5", "size": "456"}, + } + + probe = media_shrinker._parse_probe_payload(payload, Path("stream.mp4")) + + self.assertEqual(probe.duration_seconds, 123.5) + class PlanningTests(unittest.TestCase): def test_pcm_wav_uses_lossless_flac_first_and_preserves_container_metadata(