Files
ffmpeg_wrapper/scripts/fetch_ffmpeg.py
Sean MTS fb2b5ce91f Add Unlicense and ship ffmpeg's GPL license in release archives
The project is now public domain under the Unlicense (LICENSE, README).

The packaged archives bundle GPL ffmpeg/ffprobe builds but did not include
their license. fetch_ffmpeg.py now extracts the build's LICENSE.txt into
ffmpeg/LICENSE.txt, and build.py --ffmpeg-dir copies it into the archive as
ffmpeg-LICENSE.txt, failing before the PyInstaller run if it can't find one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:18:55 +00:00

104 lines
3.7 KiB
Python

"""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())