feat: isolate probe failures in reports

This commit is contained in:
kim
2026-04-02 18:38:09 +08:00
parent bc242f8a31
commit 7dd47a51ff
8 changed files with 293 additions and 27 deletions
+36 -1
View File
@@ -1,7 +1,7 @@
from pathlib import Path
from video_metadata_analyzer.analysis import build_summary
from video_metadata_analyzer.models import VideoMetadata
from video_metadata_analyzer.models import ProbeFailure, VideoMetadata
def test_build_summary_computes_totals_and_histograms() -> None:
@@ -81,3 +81,38 @@ def test_build_summary_computes_totals_and_histograms() -> None:
("1280x720", 1),
("3840x2160", 1),
]
def test_build_summary_tracks_successful_and_failed_files() -> None:
records = [
VideoMetadata(
path=Path("a.mp4"),
size_bytes=1024,
duration_seconds=10,
format_name="mov,mp4,m4a,3gp,3g2,mj2",
bit_rate=1_000_000,
video_codec="h264",
audio_codec="aac",
width=1920,
height=1080,
frame_rate=30.0,
video_stream_count=1,
audio_stream_count=1,
subtitle_stream_count=0,
)
]
failures = [
ProbeFailure(
path=Path("broken.mp4"),
error_type="CalledProcessError",
error_message="moov atom not found",
)
]
summary = build_summary(records, failures=failures)
assert summary.total_files == 2
assert summary.successful_files == 1
assert summary.failed_files == 1
assert summary.total_size_bytes == 1024
assert summary.total_duration_seconds == 10
+64
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import subprocess
from pathlib import Path
from video_metadata_analyzer.cli import main
@@ -50,3 +51,66 @@ def test_main_scans_input_directory_and_writes_reports(tmp_path: Path) -> None:
assert (output_dir / "report.json").exists()
assert (output_dir / "files.csv").exists()
assert (output_dir / "report.html").exists()
def test_main_continues_when_single_file_probe_fails(tmp_path: Path) -> None:
input_dir = tmp_path / "videos"
input_dir.mkdir()
good_sample = input_dir / "good.mp4"
bad_sample = input_dir / "bad.mp4"
good_sample.write_bytes(b"x" * 2048)
bad_sample.write_bytes(b"y" * 1024)
output_dir = tmp_path / "output"
def fake_runner(command: list[str]) -> str:
if command[:2] == ["ffprobe", "-version"]:
return "ffprobe version 8.0.1\nbuilt with ...\n"
if command[:2] == ["ffmpeg", "-version"]:
return "ffmpeg version 8.0.1\nbuilt with ...\n"
if command[0] == "ffprobe" and command[-1] == str(good_sample):
return json.dumps(
{
"format": {
"filename": str(good_sample),
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"duration": "8.000000",
"bit_rate": "1024000",
"size": "2048",
},
"streams": [
{
"codec_type": "video",
"codec_name": "h264",
"width": 1280,
"height": 720,
"avg_frame_rate": "25/1",
}
],
}
)
if command[0] == "ffprobe" and command[-1] == str(bad_sample):
raise subprocess.CalledProcessError(
returncode=1,
cmd=command,
stderr="moov atom not found",
)
raise AssertionError(f"unexpected command: {command}")
exit_code = main(
[str(input_dir), "--output-dir", str(output_dir)],
command_runner=fake_runner,
)
assert exit_code == 0
payload = json.loads((output_dir / "report.json").read_text(encoding="utf-8"))
assert payload["summary"]["total_files"] == 2
assert payload["summary"]["successful_files"] == 1
assert payload["summary"]["failed_files"] == 1
assert payload["files"][0]["path"] == good_sample.as_posix()
assert payload["failures"] == [
{
"path": bad_sample.as_posix(),
"error_type": "CalledProcessError",
"error_message": "moov atom not found",
}
]
+18 -4
View File
@@ -4,7 +4,7 @@ import json
from pathlib import Path
from video_metadata_analyzer.analysis import build_summary
from video_metadata_analyzer.models import VideoMetadata
from video_metadata_analyzer.models import ProbeFailure, VideoMetadata
from video_metadata_analyzer.reporting import write_report_bundle
@@ -25,12 +25,18 @@ def test_write_report_bundle_creates_json_csv_and_html(tmp_path: Path) -> None:
audio_stream_count=1,
subtitle_stream_count=0,
)
summary = build_summary([record])
failure = ProbeFailure(
path=Path("videos/broken.mp4"),
error_type="CalledProcessError",
error_message="moov atom not found",
)
summary = build_summary([record], failures=[failure])
paths = write_report_bundle(
output_dir=output_dir,
scanned_inputs=[Path("videos")],
records=[record],
failures=[failure],
summary=summary,
tool_versions={"ffprobe": "ffprobe version x", "ffmpeg": "ffmpeg version y"},
)
@@ -41,15 +47,23 @@ def test_write_report_bundle_creates_json_csv_and_html(tmp_path: Path) -> None:
assert paths["html"].exists()
payload = json.loads(paths["json"].read_text(encoding="utf-8"))
assert payload["summary"]["total_files"] == 1
assert payload["summary"]["total_files"] == 2
assert payload["summary"]["successful_files"] == 1
assert payload["summary"]["failed_files"] == 1
assert payload["tool_versions"]["ffmpeg"] == "ffmpeg version y"
assert payload["files"][0]["path"] == "videos/sample.mp4"
assert payload["failures"][0]["path"] == "videos/broken.mp4"
assert payload["failures"][0]["error_message"] == "moov atom not found"
csv_text = paths["csv"].read_text(encoding="utf-8")
assert "path,size_bytes,duration_seconds" in csv_text
assert "status,path,size_bytes,duration_seconds" in csv_text
assert "videos/sample.mp4" in csv_text
assert "videos/broken.mp4" in csv_text
assert "CalledProcessError" in csv_text
html_text = paths["html"].read_text(encoding="utf-8")
assert "视频元数据分析报告" in html_text
assert "时长分布" in html_text
assert "错误清单" in html_text
assert "moov atom not found" in html_text
assert "<svg" in html_text