Version 1 OneShot
This commit is contained in:
102
ffwrap/tools.py
Normal file
102
ffwrap/tools.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Locating ffmpeg / ffprobe and running them without flashing console windows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
IS_WINDOWS = sys.platform.startswith("win")
|
||||
EXE_SUFFIX = ".exe" if IS_WINDOWS else ""
|
||||
|
||||
# Keep ffprobe/ffmpeg helper calls from popping up a console on Windows.
|
||||
_NO_WINDOW = {"creationflags": subprocess.CREATE_NO_WINDOW} if IS_WINDOWS else {}
|
||||
|
||||
|
||||
def app_dir() -> Path:
|
||||
"""Directory holding the executable (frozen) or the project root (source)."""
|
||||
if getattr(sys, "frozen", False):
|
||||
return Path(sys.executable).resolve().parent
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def run_quiet(args: list[str], timeout: float | None = 60) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=timeout,
|
||||
**_NO_WINDOW,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FFmpegTools:
|
||||
ffmpeg: str
|
||||
ffprobe: str
|
||||
version: str = ""
|
||||
encoders: set[str] = field(default_factory=set)
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return bool(self.ffmpeg and self.ffprobe)
|
||||
|
||||
|
||||
def _candidate_dirs(configured_dir: str | None) -> list[Path]:
|
||||
dirs: list[Path] = []
|
||||
if configured_dir:
|
||||
dirs.append(Path(configured_dir))
|
||||
base = app_dir()
|
||||
dirs += [base, base / "ffmpeg", base / "ffmpeg" / "bin", base / "bin"]
|
||||
return dirs
|
||||
|
||||
|
||||
def _find_in(dirs: list[Path], name: str) -> str | None:
|
||||
for d in dirs:
|
||||
p = d / f"{name}{EXE_SUFFIX}"
|
||||
if p.is_file() and (IS_WINDOWS or os.access(p, os.X_OK)):
|
||||
return str(p)
|
||||
return None
|
||||
|
||||
|
||||
def locate_tools(configured_dir: str | None = None) -> FFmpegTools | None:
|
||||
"""Find ffmpeg + ffprobe: configured dir, next to the app, then PATH."""
|
||||
dirs = _candidate_dirs(configured_dir)
|
||||
ffmpeg = _find_in(dirs, "ffmpeg")
|
||||
if ffmpeg:
|
||||
# Prefer the ffprobe that ships alongside the chosen ffmpeg.
|
||||
ffprobe = _find_in([Path(ffmpeg).parent], "ffprobe") or _find_in(dirs, "ffprobe")
|
||||
else:
|
||||
ffmpeg = shutil.which("ffmpeg")
|
||||
ffprobe = None
|
||||
if ffmpeg and not ffprobe:
|
||||
ffprobe = _find_in([Path(ffmpeg).parent], "ffprobe") or shutil.which("ffprobe")
|
||||
if not ffmpeg or not ffprobe:
|
||||
return None
|
||||
|
||||
tools = FFmpegTools(ffmpeg=ffmpeg, ffprobe=ffprobe)
|
||||
try:
|
||||
out = run_quiet([ffmpeg, "-hide_banner", "-version"], timeout=15).stdout.decode(errors="replace")
|
||||
first = out.splitlines()[0] if out else ""
|
||||
m = re.match(r"ffmpeg version (\S+)", first)
|
||||
tools.version = m.group(1) if m else first
|
||||
enc = run_quiet([ffmpeg, "-hide_banner", "-encoders"], timeout=15).stdout.decode(errors="replace")
|
||||
tools.encoders = parse_encoders(enc)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
return tools
|
||||
|
||||
|
||||
def parse_encoders(text: str) -> set[str]:
|
||||
names = set()
|
||||
for line in text.splitlines():
|
||||
# " V....D libx264 libx264 H.264 ..."
|
||||
m = re.match(r"^\s*([VAS])[\w.]{5}\s+(\S+)\s", line)
|
||||
if m and m.group(2) != "=":
|
||||
names.add(m.group(2))
|
||||
return names
|
||||
Reference in New Issue
Block a user