Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| from fractions import Fraction | |
| from pathlib import Path | |
| from typing import Any | |
| from app.core.config import Settings | |
| from app.core.exceptions import ProcessingError | |
| from app.core.logger import get_logger | |
| logger = get_logger(__name__) | |
| class FFprobeService: | |
| def __init__(self, settings: Settings) -> None: | |
| self.settings = settings | |
| self._semaphore = asyncio.Semaphore(settings.max_workers) | |
| async def probe(self, path: Path) -> dict[str, Any]: | |
| command = [ | |
| self.settings.ffprobe_binary, | |
| "-v", | |
| "error", | |
| "-show_format", | |
| "-show_streams", | |
| "-print_format", | |
| "json", | |
| str(path), | |
| ] | |
| try: | |
| async with self._semaphore: | |
| process = await asyncio.create_subprocess_exec( | |
| *command, | |
| stdout=asyncio.subprocess.PIPE, | |
| stderr=asyncio.subprocess.PIPE, | |
| ) | |
| stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=120) | |
| except FileNotFoundError as exc: | |
| raise ProcessingError("FFprobe is not installed or not available") from exc | |
| except asyncio.TimeoutError as exc: | |
| if "process" in locals(): | |
| process.kill() | |
| await process.wait() | |
| raise ProcessingError("FFprobe timed out") from exc | |
| if process.returncode != 0: | |
| message = stderr.decode("utf-8", errors="replace")[-2_000:] | |
| logger.error("ffprobe failed", extra={"command": command, "stderr": message}) | |
| raise ProcessingError("Unable to read media metadata") | |
| try: | |
| raw = json.loads(stdout) | |
| except json.JSONDecodeError as exc: | |
| raise ProcessingError("FFprobe returned invalid metadata") from exc | |
| return self._normalize(raw) | |
| def _normalize(cls, raw: dict[str, Any]) -> dict[str, Any]: | |
| streams = raw.get("streams", []) | |
| fmt = raw.get("format", {}) | |
| videos = [stream for stream in streams if stream.get("codec_type") == "video"] | |
| audios = [stream for stream in streams if stream.get("codec_type") == "audio"] | |
| subtitles = [stream for stream in streams if stream.get("codec_type") == "subtitle"] | |
| video = videos[0] if videos else {} | |
| tags = fmt.get("tags", {}) | |
| video_tags = video.get("tags", {}) | |
| rotation = video_tags.get("rotate") | |
| for item in video.get("side_data_list", []): | |
| if "rotation" in item: | |
| rotation = item["rotation"] | |
| duration = cls._float(fmt.get("duration")) | |
| if duration is None: | |
| duration = cls._float(video.get("duration")) | |
| return { | |
| "duration": duration, | |
| "resolution": ( | |
| { | |
| "width": video.get("width"), | |
| "height": video.get("height"), | |
| } | |
| if video | |
| else None | |
| ), | |
| "fps": cls._fps(video.get("avg_frame_rate") or video.get("r_frame_rate")), | |
| "bitrate": cls._int(fmt.get("bit_rate")), | |
| "codec": video.get("codec_name") or (audios[0].get("codec_name") if audios else None), | |
| "video_streams": [cls._stream_summary(stream) for stream in videos], | |
| "audio_streams": [cls._stream_summary(stream) for stream in audios], | |
| "subtitle_streams": [cls._stream_summary(stream) for stream in subtitles], | |
| "rotation": cls._int(rotation), | |
| "container": fmt.get("format_name"), | |
| "creation_date": tags.get("creation_time") or video_tags.get("creation_time"), | |
| "size": cls._int(fmt.get("size")), | |
| "tags": tags, | |
| } | |
| def _stream_summary(cls, stream: dict[str, Any]) -> dict[str, Any]: | |
| return { | |
| "index": stream.get("index"), | |
| "codec": stream.get("codec_name"), | |
| "profile": stream.get("profile"), | |
| "pixel_format": stream.get("pix_fmt"), | |
| "field_order": stream.get("field_order"), | |
| "sample_aspect_ratio": stream.get("sample_aspect_ratio"), | |
| "frame_count": cls._int(stream.get("nb_frames")), | |
| "bitrate": cls._int(stream.get("bit_rate")), | |
| "sample_rate": cls._int(stream.get("sample_rate")), | |
| "channels": stream.get("channels"), | |
| "width": stream.get("width"), | |
| "height": stream.get("height"), | |
| "language": stream.get("tags", {}).get("language"), | |
| } | |
| def _fps(value: str | None) -> float | None: | |
| if not value or value == "0/0": | |
| return None | |
| try: | |
| return round(float(Fraction(value)), 4) | |
| except (ValueError, ZeroDivisionError): | |
| return None | |
| def _int(value: Any) -> int | None: | |
| try: | |
| return int(value) | |
| except (TypeError, ValueError): | |
| return None | |
| def _float(value: Any) -> float | None: | |
| try: | |
| return round(float(value), 6) | |
| except (TypeError, ValueError): | |
| return None | |