Files
francwa c303efea48 refactor(probe): consolidate full probe() into MediaProber port
Add probe(video) -> MediaInfo | None to the MediaProber Protocol and
implement it on FfprobeMediaProber. The standalone
alfred/infrastructure/filesystem/ffprobe.py module is removed; all
callers (analyze_release / probe_media tools, testing scripts) now go
through the adapter.

Tests for the probe path moved to tests/infrastructure/test_ffprobe_prober.py
(patching subprocess.run at the adapter module level).

Unblocks the upcoming inspect_release orchestrator, which needs the
port — not a free function — to compose parse + main-video selection
+ probe in one shot.
2026-05-20 09:11:24 +02:00

165 lines
4.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
probe_video.py — Display MediaInfo extracted by ffprobe for a video file.
Usage:
uv run testing/probe_video.py /path/to/video.mkv
uv run testing/probe_video.py /path/to/video.mkv --no-color
"""
import argparse
import sys
from pathlib import Path
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(_PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(_PROJECT_ROOT))
# ---------------------------------------------------------------------------
# Colours
# ---------------------------------------------------------------------------
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
RED = "\033[31m"
CYAN = "\033[36m"
BLUE = "\033[34m"
USE_COLOR = True
def c(text: str, *codes: str) -> str:
if not USE_COLOR:
return str(text)
return "".join(codes) + str(text) + RESET
def kv(key: str, val: str, indent: int = 4, color: str = CYAN) -> None:
print(f"{' ' * indent}{c(key + ':', BOLD)} {c(val, color)}")
def section(title: str) -> None:
print()
print(f" {c('' + title, BOLD, BLUE)}")
def hr() -> None:
print(c("" * 70, DIM))
# ---------------------------------------------------------------------------
# Formatting helpers
# ---------------------------------------------------------------------------
def fmt_duration(seconds: float) -> str:
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
if h:
return f"{h}h {m:02d}m {s:02d}s"
return f"{m}m {s:02d}s"
def fmt_channels(channels: int | None, layout: str | None) -> str:
parts = []
if channels is not None:
parts.append(str(channels) + "ch")
if layout:
parts.append(f"({layout})")
return " ".join(parts) if parts else ""
def flag(val: bool) -> str:
return c("yes", GREEN) if val else c("no", DIM)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
global USE_COLOR
parser = argparse.ArgumentParser(description="Probe a video file with ffprobe")
parser.add_argument("file", help="Path to the video file")
parser.add_argument("--no-color", action="store_true")
args = parser.parse_args()
if args.no_color or not sys.stdout.isatty():
USE_COLOR = False
path = Path(args.file)
if not path.exists():
print(c(f"Error: {path} does not exist", RED), file=sys.stderr)
sys.exit(1)
from alfred.infrastructure.probe import FfprobeMediaProber
info = FfprobeMediaProber().probe(path)
if info is None:
print(c("Error: ffprobe failed to probe the file", RED), file=sys.stderr)
sys.exit(1)
print()
print(c("" * 70, BOLD))
print(c(f" {path.name}", BOLD, CYAN))
print(c(f" {path}", DIM))
print(c("" * 70, BOLD))
# --- Video ---
section("Video")
kv("codec", info.video_codec or c("", DIM))
kv("resolution", info.resolution or c("", DIM))
if info.width and info.height:
kv("dimensions", f"{info.width} × {info.height}")
if info.duration_seconds is not None:
kv("duration", fmt_duration(info.duration_seconds))
if info.bitrate_kbps is not None:
kv("bitrate", f"{info.bitrate_kbps} kbps")
# --- Audio ---
section(f"Audio {c(str(len(info.audio_tracks)) + ' track(s)', DIM)}")
if not info.audio_tracks:
print(f" {c('no audio tracks found', DIM)}")
for track in info.audio_tracks:
lang = track.language or "und"
default_marker = f" {c('default', GREEN, DIM)}" if track.is_default else ""
print(f" {c(f'[{track.index}]', BOLD)} {c(lang, YELLOW)}{default_marker}")
kv("codec", track.codec or c("", DIM), indent=8)
kv("channels", fmt_channels(track.channels, track.channel_layout), indent=8)
# --- Subtitles ---
section(f"Subtitles {c(str(len(info.subtitle_tracks)) + ' track(s)', DIM)}")
if not info.subtitle_tracks:
print(f" {c('no embedded subtitle tracks', DIM)}")
for track in info.subtitle_tracks:
lang = track.language or "und"
markers = []
if track.is_default:
markers.append(c("default", GREEN, DIM))
if track.is_forced:
markers.append(c("forced", YELLOW, DIM))
marker_str = (" " + " ".join(markers)) if markers else ""
print(f" {c(f'[{track.index}]', BOLD)} {c(lang, YELLOW)}{marker_str}")
kv("codec", track.codec or c("", DIM), indent=8)
# --- Summary ---
print()
hr()
multi = c("yes", GREEN) if info.is_multi_audio else c("no", DIM)
langs = ", ".join(info.audio_languages) if info.audio_languages else c("", DIM)
print(
f" {c('multi-audio:', BOLD)} {multi} {c('languages:', BOLD)} {c(langs, CYAN)}"
)
hr()
print()
if __name__ == "__main__":
main()