Version 1 OneShot
This commit is contained in:
79
scripts/fetch_ffmpeg.py
Normal file
79
scripts/fetch_ffmpeg.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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)
|
||||
DEST.mkdir(parents=True, exist_ok=True)
|
||||
wanted = {f"ffmpeg{exe}", f"ffprobe{exe}"}
|
||||
if archive.suffix == ".zip":
|
||||
with zipfile.ZipFile(archive) as z:
|
||||
for info in z.infolist():
|
||||
name = Path(info.filename).name
|
||||
if name in wanted and "/bin/" in info.filename:
|
||||
(DEST / name).write_bytes(z.read(info))
|
||||
else:
|
||||
with tarfile.open(archive) as t:
|
||||
for m in t.getmembers():
|
||||
name = Path(m.name).name
|
||||
if name in wanted and "/bin/" in m.name:
|
||||
with t.extractfile(m) as src, open(DEST / name, "wb") as out:
|
||||
shutil.copyfileobj(src, out)
|
||||
(DEST / name).chmod(0o755)
|
||||
missing = [n for n in wanted if not (DEST / n).is_file()]
|
||||
if missing:
|
||||
raise SystemExit(f"Archive did not contain {missing}")
|
||||
print(f"Installed ffmpeg and ffprobe into {DEST}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
69
scripts/make_icon.py
Normal file
69
scripts/make_icon.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Render assets/icon.png and a multi-size assets/icon.ico. Run once; outputs are committed."""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtCore import QBuffer, QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QGuiApplication, QImage, QLinearGradient, QPainter, QPainterPath, QPolygonF
|
||||
|
||||
ASSETS = Path(__file__).resolve().parent.parent / "assets"
|
||||
|
||||
|
||||
def render(size: int) -> QImage:
|
||||
img = QImage(size, size, QImage.Format_ARGB32)
|
||||
img.fill(Qt.transparent)
|
||||
p = QPainter(img)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
s = size / 256
|
||||
grad = QLinearGradient(0, 0, 0, size)
|
||||
grad.setColorAt(0, QColor("#2f80ed"))
|
||||
grad.setColorAt(1, QColor("#1b4f9c"))
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(QRectF(8 * s, 8 * s, 240 * s, 240 * s), 52 * s, 52 * s)
|
||||
p.fillPath(path, grad)
|
||||
# Film strip holes along the top and bottom edges.
|
||||
p.setBrush(QColor(255, 255, 255, 70))
|
||||
p.setPen(Qt.NoPen)
|
||||
for i in range(6):
|
||||
x = (38 + i * 34) * s
|
||||
p.drawRoundedRect(QRectF(x, 22 * s, 18 * s, 14 * s), 3 * s, 3 * s)
|
||||
p.drawRoundedRect(QRectF(x, 220 * s, 18 * s, 14 * s), 3 * s, 3 * s)
|
||||
# Two chevrons: "convert from -> to".
|
||||
p.setBrush(QColor("white"))
|
||||
for dx in (0, 62):
|
||||
p.drawPolygon(QPolygonF([QPointF((70 + dx) * s, 72 * s), QPointF((136 + dx) * s, 128 * s),
|
||||
QPointF((70 + dx) * s, 184 * s), QPointF((70 + dx) * s, 152 * s),
|
||||
QPointF((100 + dx) * s, 128 * s), QPointF((70 + dx) * s, 104 * s)]))
|
||||
p.end()
|
||||
return img
|
||||
|
||||
|
||||
def png_bytes(img: QImage) -> bytes:
|
||||
buf = QBuffer()
|
||||
buf.open(QBuffer.WriteOnly)
|
||||
img.save(buf, "PNG")
|
||||
return bytes(buf.data())
|
||||
|
||||
|
||||
def write_ico(path: Path, sizes=(16, 24, 32, 48, 64, 128, 256)) -> None:
|
||||
images = [png_bytes(render(n)) for n in sizes]
|
||||
header = struct.pack("<HHH", 0, 1, len(images))
|
||||
offset = 6 + 16 * len(images)
|
||||
entries, blobs = b"", b""
|
||||
for n, data in zip(sizes, images):
|
||||
entries += struct.pack("<BBBBHHII", n % 256, n % 256, 0, 0, 1, 32, len(data), offset)
|
||||
offset += len(data)
|
||||
blobs += data
|
||||
path.write_bytes(header + entries + blobs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QGuiApplication(sys.argv)
|
||||
ASSETS.mkdir(exist_ok=True)
|
||||
render(256).save(str(ASSETS / "icon.png"))
|
||||
write_ico(ASSETS / "icon.ico")
|
||||
print("wrote", ASSETS / "icon.png", ASSETS / "icon.ico")
|
||||
26
scripts/smoke_test.py
Normal file
26
scripts/smoke_test.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Launch a built executable headless, let it start up, and check it found ffmpeg.
|
||||
|
||||
python scripts/smoke_test.py dist/package/FFmpegWrapper/FFmpegWrapper[.exe] [shot.png]
|
||||
|
||||
Exits non-zero if the app crashes, hangs, or reports that ffmpeg is missing.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
exe = Path(sys.argv[1]).resolve()
|
||||
shot = Path(sys.argv[2] if len(sys.argv) > 2 else "smoke-test.png").resolve()
|
||||
shot.unlink(missing_ok=True)
|
||||
env = dict(os.environ, QT_QPA_PLATFORM="offscreen", FFWRAP_SMOKE_TEST=str(shot))
|
||||
res = subprocess.run([str(exe)], env=env, timeout=180)
|
||||
if res.returncode != 0 or not shot.is_file():
|
||||
sys.exit(f"Smoke test failed (exit code {res.returncode}, screenshot {'ok' if shot.is_file() else 'missing'})")
|
||||
print(f"Smoke test passed; screenshot saved to {shot}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user