| from __future__ import annotations |
|
|
| import json |
| import mimetypes |
| import subprocess |
| from dataclasses import asdict |
| from pathlib import Path |
|
|
| from renderer.core.models import AssetMetadata |
| from renderer.core.utils import read_json, write_json |
|
|
|
|
| class AssetProbe: |
| def __init__(self, cache_path: Path) -> None: |
| self.cache_path = cache_path |
| self.cache: dict[str, dict] = read_json(cache_path, {}) |
|
|
| def probe(self, path: str | Path) -> AssetMetadata: |
| media = Path(path) |
| stat = media.stat() |
| key = str(media.resolve()) |
| cached = self.cache.get(key) |
| if cached and cached.get("size_bytes") == stat.st_size and cached.get("mtime") == stat.st_mtime: |
| return AssetMetadata(**cached) |
|
|
| raw = self._ffprobe(media) |
| streams = raw.get("streams", []) |
| fmt = raw.get("format", {}) |
| video_stream = next((s for s in streams if s.get("codec_type") == "video"), {}) |
| audio_stream = next((s for s in streams if s.get("codec_type") == "audio"), {}) |
| metadata = AssetMetadata( |
| path=str(media), |
| mime_type=mimetypes.guess_type(str(media))[0] or "application/octet-stream", |
| size_bytes=stat.st_size, |
| mtime=stat.st_mtime, |
| duration=float(fmt.get("duration") or video_stream.get("duration") or audio_stream.get("duration") or 0), |
| width=_int_or_none(video_stream.get("width")), |
| height=_int_or_none(video_stream.get("height")), |
| fps=_parse_fps(video_stream.get("avg_frame_rate") or video_stream.get("r_frame_rate")), |
| bitrate=_int_or_none(fmt.get("bit_rate")), |
| video_codec=video_stream.get("codec_name"), |
| audio_codec=audio_stream.get("codec_name"), |
| has_audio=bool(audio_stream), |
| streams=streams, |
| ) |
| self.cache[key] = asdict(metadata) |
| write_json(self.cache_path, self.cache) |
| return metadata |
|
|
| @staticmethod |
| def _ffprobe(path: Path) -> dict: |
| command = [ |
| "ffprobe", |
| "-v", |
| "error", |
| "-show_format", |
| "-show_streams", |
| "-print_format", |
| "json", |
| str(path), |
| ] |
| result = subprocess.run(command, check=True, capture_output=True, text=True) |
| return json.loads(result.stdout or "{}") |
|
|
|
|
| def _parse_fps(value: str | None) -> float | None: |
| if not value or value == "0/0": |
| return None |
| if "/" in value: |
| num, den = value.split("/", 1) |
| den_f = float(den) |
| return float(num) / den_f if den_f else None |
| return float(value) |
|
|
|
|
| def _int_or_none(value: object) -> int | None: |
| if value in (None, ""): |
| return None |
| return int(value) |
|
|