feat: Track yt-dlp releases in cinema-tools updates

This commit is contained in:
pleb
2026-10-22 08:24:21 -07:00
parent 1becf8366d
commit 455c9fd2c6
5 changed files with 278 additions and 14 deletions
+68 -11
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import argparse
import hashlib
import os
import re
import shutil
import sys
import tempfile
@@ -15,8 +16,10 @@ from zipfile import ZIP_STORED, ZipFile, ZipInfo
PLUGIN_ID = "cinema-tools"
DEFAULT_INSTANCE = "1.44.1"
YT_DLP_REPO = "yt-dlp/yt-dlp"
YT_DLP_ASSET = "yt-dlp.exe"
YT_DLP_VERSION = "2026.07.04"
YT_DLP_URL = f"https://github.com/yt-dlp/yt-dlp/releases/download/{YT_DLP_VERSION}/yt-dlp.exe"
YT_DLP_URL = f"https://github.com/{YT_DLP_REPO}/releases/download/{YT_DLP_VERSION}/{YT_DLP_ASSET}"
YT_DLP_SHA256 = "52fe3c26dcf71fbdc85b528589020bb0b8e383155cfa81b64dd447bbe35e24b8"
FFMPEG_VERSION = "8.1.2"
@@ -31,6 +34,36 @@ ASSET_NAME = f"CinemaTools-yt-dlp-{YT_DLP_VERSION}-ffmpeg-{FFMPEG_VERSION}.zip"
BUNDLE_SHA256 = "4d299e40f747abb9777838542ca68ec450e0b86e6eb11af341e6bd568f47edf2"
ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0)
_TAG_RE = re.compile(r"^yt-dlp-(?P<yt_dlp>.+)-ffmpeg-(?P<ffmpeg>[^-]+)$")
def bundle_tag(yt_dlp_version: str) -> str:
return f"yt-dlp-{yt_dlp_version}-ffmpeg-{FFMPEG_VERSION}"
def asset_name(yt_dlp_version: str) -> str:
return f"CinemaTools-{bundle_tag(yt_dlp_version)}.zip"
def yt_dlp_url(yt_dlp_version: str) -> str:
return f"https://github.com/{YT_DLP_REPO}/releases/download/{yt_dlp_version}/{YT_DLP_ASSET}"
def yt_dlp_version_from_tag(tag: str | None) -> str | None:
match = _TAG_RE.match(tag or "")
return match.group("yt_dlp") if match else None
def lock_reason(yt_dlp_version: str, yt_dlp_sha256: str, instance: str) -> str:
return (
"Helper-managed Windows executable bundle for Cinema's hardcoded Proton paths "
"Libs/yt-dlp.exe and Libs/ffmpeg.exe. Rebuild with PYTHONPATH=src python -m "
f"plugin_helper.recipes.cinema_tools --instance {instance} --yt-dlp-version {yt_dlp_version} "
f"--yt-dlp-sha256 {yt_dlp_sha256}. Sources: {yt_dlp_url(yt_dlp_version)} with sha256 "
f"{yt_dlp_sha256}, and {FFMPEG_URL} with sha256 {FFMPEG_ZIP_SHA256}; the recipe extracts "
f"*/bin/ffmpeg.exe with sha256 {FFMPEG_EXE_SHA256} and writes a deterministic root zip."
)
@dataclass(frozen=True)
class BuiltAsset:
@@ -94,18 +127,27 @@ def _write_member(archive: ZipFile, name: str, data: bytes) -> None:
archive.writestr(info, data)
def build_asset(output: Path, *, work_dir: Path | None = None) -> BuiltAsset:
def build_asset(
output: Path,
*,
yt_dlp_version: str = YT_DLP_VERSION,
yt_dlp_sha256: str = YT_DLP_SHA256,
work_dir: Path | None = None,
) -> BuiltAsset:
output = output.expanduser()
output.parent.mkdir(parents=True, exist_ok=True)
# The bundle hash is only known ahead of time for the pinned yt-dlp release.
expected_bundle_sha256 = BUNDLE_SHA256 if yt_dlp_version == YT_DLP_VERSION else None
name = asset_name(yt_dlp_version)
with tempfile.TemporaryDirectory(dir=work_dir) as tmp:
tmp_path = Path(tmp)
yt_dlp_path = tmp_path / "yt-dlp.exe"
yt_dlp_path = tmp_path / YT_DLP_ASSET
ffmpeg_zip_path = tmp_path / f"ffmpeg-{FFMPEG_VERSION}-essentials_build.zip"
staged_output = tmp_path / ASSET_NAME
staged_output = tmp_path / name
_download(YT_DLP_URL, yt_dlp_path)
_verify(yt_dlp_path, YT_DLP_SHA256, "yt-dlp.exe")
_download(yt_dlp_url(yt_dlp_version), yt_dlp_path)
_verify(yt_dlp_path, yt_dlp_sha256, YT_DLP_ASSET)
_download(FFMPEG_URL, ffmpeg_zip_path)
_verify(ffmpeg_zip_path, FFMPEG_ZIP_SHA256, "ffmpeg essentials zip")
@@ -118,9 +160,9 @@ def build_asset(output: Path, *, work_dir: Path | None = None) -> BuiltAsset:
_write_member(archive, "Libs/ffmpeg.exe", ffmpeg_data)
actual_bundle_sha256 = _sha256_file(staged_output)
if actual_bundle_sha256 != BUNDLE_SHA256:
if expected_bundle_sha256 and actual_bundle_sha256 != expected_bundle_sha256:
raise ValueError(
f"{ASSET_NAME} sha256 mismatch: expected {BUNDLE_SHA256}, got {actual_bundle_sha256}"
f"{name} sha256 mismatch: expected {expected_bundle_sha256}, got {actual_bundle_sha256}"
)
shutil.move(str(staged_output), output)
@@ -142,15 +184,30 @@ def build_parser() -> argparse.ArgumentParser:
help="Exact output path; defaults to <state-dir>/cache/downloads/<instance>/cinema-tools/<asset>",
)
parser.add_argument("--work-dir", type=Path, help="Parent directory for temporary downloads")
parser.add_argument("--yt-dlp-version", default=YT_DLP_VERSION, help="yt-dlp release tag to bundle")
parser.add_argument(
"--yt-dlp-sha256",
help="Expected yt-dlp.exe sha256; required when --yt-dlp-version differs from the pinned release",
)
return parser
def run(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
output = args.output or args.state_dir / "cache" / "downloads" / args.instance / PLUGIN_ID / ASSET_NAME
yt_dlp_sha256 = args.yt_dlp_sha256 or (YT_DLP_SHA256 if args.yt_dlp_version == YT_DLP_VERSION else None)
if not yt_dlp_sha256:
parser.error("--yt-dlp-sha256 is required with a non-default --yt-dlp-version")
output = args.output or (
args.state_dir / "cache" / "downloads" / args.instance / PLUGIN_ID / asset_name(args.yt_dlp_version)
)
try:
built = build_asset(output, work_dir=args.work_dir)
built = build_asset(
output,
yt_dlp_version=args.yt_dlp_version,
yt_dlp_sha256=yt_dlp_sha256,
work_dir=args.work_dir,
)
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
@@ -158,7 +215,7 @@ def run(argv: list[str] | None = None) -> int:
print(f"Size: {built.size}")
print(f"SHA-256: {built.sha256}")
print("Members:")
print(f" Libs/yt-dlp.exe {YT_DLP_SHA256}")
print(f" Libs/yt-dlp.exe {yt_dlp_sha256}")
print(f" Libs/ffmpeg.exe {FFMPEG_EXE_SHA256}")
return 0