Version 1 OneShot

This commit is contained in:
2026-10-09 18:58:50 +00:00
parent 514c13d184
commit bf0d661eed
27 changed files with 3039 additions and 0 deletions

102
ffwrap/tools.py Normal file
View 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