feat: 支持自定义时长和大小分桶粒度
This commit is contained in:
+29
-1
@@ -1,6 +1,6 @@
|
||||
from pathlib import Path
|
||||
|
||||
from video_metadata_analyzer.analysis import build_summary
|
||||
from video_metadata_analyzer.analysis import MEGABYTE, SummaryOptions, build_summary
|
||||
from video_metadata_analyzer.models import ProbeFailure, VideoMetadata
|
||||
|
||||
|
||||
@@ -145,6 +145,34 @@ def test_build_summary_uses_finer_buckets_for_large_sample_sets_with_outliers()
|
||||
assert summary.size_histogram[0].label == "0-500 MB"
|
||||
|
||||
|
||||
def test_build_summary_respects_explicit_histogram_steps() -> None:
|
||||
records = [
|
||||
_build_record("a.mp4", size_mb=10, duration_seconds=25),
|
||||
_build_record("b.mp4", size_mb=40, duration_seconds=65),
|
||||
_build_record("c.mp4", size_mb=60, duration_seconds=115),
|
||||
]
|
||||
|
||||
summary = build_summary(
|
||||
records,
|
||||
summary_options=SummaryOptions(
|
||||
duration_bucket_seconds=30,
|
||||
size_bucket_bytes=25 * MEGABYTE,
|
||||
),
|
||||
)
|
||||
|
||||
assert [(item.label, item.count) for item in summary.duration_histogram] == [
|
||||
("0-30 s", 1),
|
||||
("30-60 s", 0),
|
||||
("60-90 s", 1),
|
||||
("90-120 s", 1),
|
||||
]
|
||||
assert [(item.label, item.count) for item in summary.size_histogram] == [
|
||||
("0-25 MB", 1),
|
||||
("25-50 MB", 1),
|
||||
("50-75 MB", 1),
|
||||
]
|
||||
|
||||
|
||||
def test_build_summary_tracks_successful_and_failed_files() -> None:
|
||||
records = [
|
||||
VideoMetadata(
|
||||
|
||||
@@ -53,6 +53,72 @@ def test_main_scans_input_directory_and_writes_reports(tmp_path: Path) -> None:
|
||||
assert (output_dir / "report.html").exists()
|
||||
|
||||
|
||||
def test_main_writes_reports_with_explicit_histogram_granularity(tmp_path: Path) -> None:
|
||||
input_dir = tmp_path / "videos"
|
||||
input_dir.mkdir()
|
||||
sample = input_dir / "sample.mp4"
|
||||
sample.write_bytes(b"x" * 2048)
|
||||
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":
|
||||
return json.dumps(
|
||||
{
|
||||
"format": {
|
||||
"filename": str(sample),
|
||||
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||
"duration": "65.000000",
|
||||
"bit_rate": "1024000",
|
||||
"size": str(60 * 1024 * 1024),
|
||||
},
|
||||
"streams": [
|
||||
{
|
||||
"codec_type": "video",
|
||||
"codec_name": "h264",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"avg_frame_rate": "25/1",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
raise AssertionError(f"unexpected command: {command}")
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
str(input_dir),
|
||||
"--output-dir",
|
||||
str(output_dir),
|
||||
"--duration-bucket-seconds",
|
||||
"30",
|
||||
"--size-bucket-megabytes",
|
||||
"25",
|
||||
],
|
||||
command_runner=fake_runner,
|
||||
)
|
||||
|
||||
assert exit_code == 0
|
||||
payload = json.loads((output_dir / "report.json").read_text(encoding="utf-8"))
|
||||
assert payload["summary_options"] == {
|
||||
"duration_bucket_seconds": 30.0,
|
||||
"size_bucket_bytes": 25 * 1024 * 1024,
|
||||
}
|
||||
assert payload["summary"]["duration_histogram"] == [
|
||||
{"label": "0-30 s", "count": 0},
|
||||
{"label": "30-60 s", "count": 0},
|
||||
{"label": "60-90 s", "count": 1},
|
||||
]
|
||||
assert payload["summary"]["size_histogram"] == [
|
||||
{"label": "0-25 MB", "count": 0},
|
||||
{"label": "25-50 MB", "count": 0},
|
||||
{"label": "50-75 MB", "count": 1},
|
||||
]
|
||||
|
||||
|
||||
def test_main_continues_when_single_file_probe_fails(tmp_path: Path) -> None:
|
||||
input_dir = tmp_path / "videos"
|
||||
input_dir.mkdir()
|
||||
@@ -228,3 +294,83 @@ def test_main_rebuilds_html_from_existing_report_json_without_running_probe(tmp_
|
||||
assert payload["summary"]["duration_histogram"] == [{"label": "0-2 min", "count": 1}]
|
||||
assert payload["summary"]["size_histogram"] == [{"label": "0-100 MB", "count": 1}]
|
||||
assert "0-2 min" in html_text
|
||||
|
||||
|
||||
def test_main_rebuilds_report_json_with_saved_histogram_granularity_and_cli_override(tmp_path: Path) -> None:
|
||||
output_dir = tmp_path / "report"
|
||||
output_dir.mkdir()
|
||||
report_json = output_dir / "report.json"
|
||||
report_json.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"generated_at": "2026-04-02T12:00:00",
|
||||
"scanned_inputs": ["videos"],
|
||||
"tool_versions": {"ffprobe": "cached ffprobe", "ffmpeg": "cached ffmpeg"},
|
||||
"summary_options": {
|
||||
"duration_bucket_seconds": 30.0,
|
||||
"size_bucket_bytes": 25 * 1024 * 1024,
|
||||
},
|
||||
"summary": {
|
||||
"total_files": 1,
|
||||
"successful_files": 1,
|
||||
"failed_files": 0,
|
||||
"total_size_bytes": 0,
|
||||
"total_duration_seconds": 0,
|
||||
"duration_histogram": [{"label": "0-30 s", "count": 1}],
|
||||
"size_histogram": [{"label": "0-25 MB", "count": 1}],
|
||||
"video_codecs": [],
|
||||
"resolutions": [],
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"path": "videos/sample.mp4",
|
||||
"size_bytes": 60 * 1024 * 1024,
|
||||
"duration_seconds": 65.0,
|
||||
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||
"bit_rate": 1024000,
|
||||
"video_codec": "h264",
|
||||
"audio_codec": "aac",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"frame_rate": 25.0,
|
||||
"video_stream_count": 1,
|
||||
"audio_stream_count": 1,
|
||||
"subtitle_stream_count": 0,
|
||||
}
|
||||
],
|
||||
"failures": [],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def forbidden_runner(command: list[str]) -> str:
|
||||
raise AssertionError(f"should not invoke external command: {command}")
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"--report-json",
|
||||
str(report_json),
|
||||
"--duration-bucket-seconds",
|
||||
"60",
|
||||
],
|
||||
command_runner=forbidden_runner,
|
||||
)
|
||||
|
||||
assert exit_code == 0
|
||||
payload = json.loads(report_json.read_text(encoding="utf-8"))
|
||||
assert payload["summary_options"] == {
|
||||
"duration_bucket_seconds": 60.0,
|
||||
"size_bucket_bytes": 25 * 1024 * 1024,
|
||||
}
|
||||
assert payload["summary"]["duration_histogram"] == [
|
||||
{"label": "0-1 min", "count": 0},
|
||||
{"label": "1-2 min", "count": 1},
|
||||
]
|
||||
assert payload["summary"]["size_histogram"] == [
|
||||
{"label": "0-25 MB", "count": 0},
|
||||
{"label": "25-50 MB", "count": 0},
|
||||
{"label": "50-75 MB", "count": 1},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user