studio / renderer /ffmpeg /assets.py
Ava2lon's picture
Upload 170 files
345855e verified
Raw
History Blame Contribute Delete
2.76 kB
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)