"""Download a static ffmpeg + ffprobe build for this OS into ./ffmpeg/bin. Uses the GPL builds from https://github.com/BtbN/FFmpeg-Builds (newest stable release branch). The app finds ./ffmpeg/bin automatically when run from source, and build.py --ffmpeg-dir ffmpeg/bin bundles them with the executable, together with the build's license, which is saved as ./ffmpeg/LICENSE.txt. """ from __future__ import annotations import json import re import shutil import sys import tarfile import tempfile import urllib.request import zipfile from pathlib import Path DEST = Path(__file__).resolve().parent.parent / "ffmpeg" / "bin" API = "https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest" def pick_asset(assets: list[dict], plat: str) -> dict: ext = ".zip" if plat == "win64" else ".tar.xz" stable = [] for a in assets: m = re.fullmatch(rf"ffmpeg-n(\d+)\.(\d+)-latest-{plat}-gpl-\d+\.\d+{re.escape(ext)}", a["name"]) if m: stable.append(((int(m.group(1)), int(m.group(2))), a)) if stable: return max(stable, key=lambda x: x[0])[1] for a in assets: if a["name"] == f"ffmpeg-master-latest-{plat}-gpl{ext}": return a raise SystemExit(f"No ffmpeg build found for {plat}") def main() -> int: if sys.platform.startswith("win"): plat, exe = "win64", ".exe" elif sys.platform.startswith("linux"): plat, exe = "linux64", "" else: raise SystemExit("Only Windows and Linux are supported") with urllib.request.urlopen(API, timeout=30) as r: asset = pick_asset(json.load(r)["assets"], plat) print(f"Downloading {asset['name']} ({asset['size'] // 1_000_000} MB)...") with tempfile.TemporaryDirectory() as tmp: archive = Path(tmp) / asset["name"] with urllib.request.urlopen(asset["browser_download_url"], timeout=60) as r, open(archive, "wb") as f: shutil.copyfileobj(r, f) extract(archive, exe, DEST) print(f"Installed ffmpeg and ffprobe into {DEST}, and their license as {DEST.parent / 'LICENSE.txt'}") return 0 def extract(archive: Path, exe: str, dest: Path) -> None: """Copy ffmpeg + ffprobe from the archive's bin/ into dest, and its LICENSE.txt into dest's parent. A BtbN archive holds one top folder with bin/ and LICENSE.txt in it. ffmpeg is GPL, so the license has to ship with the binaries; build.py picks it up from there. """ dest.mkdir(parents=True, exist_ok=True) license_out = dest.parent / "LICENSE.txt" license_out.unlink(missing_ok=True) wanted = {f"ffmpeg{exe}", f"ffprobe{exe}"} def target(member: str) -> Path | None: name = Path(member).name if name in wanted and "/bin/" in member: return dest / name if member.strip("/").count("/") == 1 and name == "LICENSE.txt": return license_out return None if archive.suffix == ".zip": with zipfile.ZipFile(archive) as z: for info in z.infolist(): out = target(info.filename) if out: out.write_bytes(z.read(info)) else: with tarfile.open(archive) as t: for m in t.getmembers(): out = target(m.name) if m.isfile() else None if out: with t.extractfile(m) as src, open(out, "wb") as f: shutil.copyfileobj(src, f) if out.name in wanted: out.chmod(0o755) missing = [n for n in sorted(wanted) if not (dest / n).is_file()] if not license_out.is_file(): missing.append("LICENSE.txt") if missing: raise SystemExit(f"Archive did not contain {missing}") if __name__ == "__main__": sys.exit(main())