MediaRouter / app /operations /common.py
basyx's picture
Upload 142 files
fba6023 verified
Raw
History Blame Contribute Delete
3.67 kB
from __future__ import annotations
import mimetypes
import re
from collections.abc import Sequence
from pathlib import Path
from typing import Any
from app.core.exceptions import InputError, ProcessingError
from app.models.media import InputMedia, OperationResult
from app.services.ffmpeg_service import FFmpegService
VIDEO_FORMATS = {"mp4", "mkv", "webm", "mov", "avi", "m4v", "mpeg", "ts"}
AUDIO_FORMATS = {"mp3", "wav", "aac", "m4a", "flac", "ogg", "opus"}
IMAGE_FORMATS = {"jpg", "jpeg", "png", "webp", "bmp", "tiff", "gif"}
def require_inputs(inputs: Sequence[InputMedia], count: int = 1) -> None:
if len(inputs) < count:
raise InputError(f"This operation requires at least {count} media input(s)")
def output_path(output_dir: Path, stem: str, extension: str) -> Path:
clean_extension = extension.lower().lstrip(".")
if not re.fullmatch(r"[a-z0-9]{2,5}", clean_extension):
raise InputError("Invalid output format")
output_dir.mkdir(parents=True, exist_ok=True)
return output_dir / f"{stem}.{clean_extension}"
def format_param(params: dict[str, Any], default: str, allowed: set[str]) -> str:
value = str(params.get("format", default)).lower().lstrip(".")
if value not in allowed:
raise InputError(
"Unsupported output format", details={"format": value, "allowed": sorted(allowed)}
)
return value
def video_codecs(extension: str, *, crf: int = 23, preset: str = "medium") -> list[str]:
if extension == "webm":
return ["-c:v", "libvpx-vp9", "-crf", str(crf), "-b:v", "0", "-c:a", "libopus"]
if extension == "avi":
return ["-c:v", "mpeg4", "-q:v", "5", "-c:a", "libmp3lame"]
if extension in {"mpeg", "ts"}:
return ["-c:v", "mpeg2video", "-c:a", "mp2"]
return [
"-c:v",
"libx264",
"-preset",
preset,
"-crf",
str(crf),
"-c:a",
"aac",
"-b:a",
"128k",
"-movflags",
"+faststart",
]
def audio_codec(extension: str) -> list[str]:
return {
"mp3": ["-c:a", "libmp3lame", "-b:a", "192k"],
"wav": ["-c:a", "pcm_s16le"],
"aac": ["-c:a", "aac", "-b:a", "192k"],
"m4a": ["-c:a", "aac", "-b:a", "192k"],
"flac": ["-c:a", "flac"],
"ogg": ["-c:a", "libvorbis", "-q:a", "5"],
"opus": ["-c:a", "libopus", "-b:a", "128k"],
}[extension]
async def execute(
ffmpeg: FFmpegService,
args: Sequence[str | Path],
output: Path,
operation: str,
metadata: dict[str, Any] | None = None,
) -> OperationResult:
await ffmpeg.run(args, operation=operation)
if not output.is_file() or output.stat().st_size == 0:
raise ProcessingError("The operation did not produce an output file")
return OperationResult(
path=output,
filename=output.name,
mime_type=mimetypes.guess_type(output.name)[0] or "application/octet-stream",
metadata={"operation": operation, **(metadata or {})},
)
def even(value: int) -> int:
return max(2, value if value % 2 == 0 else value - 1)
def subtitle_filter_path(path: Path) -> str:
value = str(path.resolve()).replace("\\", "\\\\")
for character in (":", "'", "[", "]", ","):
value = value.replace(character, f"\\{character}")
return value
def atempo_chain(factor: float) -> str:
values: list[float] = []
remaining = factor
while remaining > 2.0:
values.append(2.0)
remaining /= 2.0
while remaining < 0.5:
values.append(0.5)
remaining /= 0.5
values.append(remaining)
return ",".join(f"atempo={value:.6g}" for value in values)