Add cinema-tools recipe
This commit is contained in:
@@ -152,6 +152,14 @@ Install assets are currently expected to already exist locally, usually under:
|
|||||||
<state-dir>/cache/downloads/<instance>/<plugin-id>/
|
<state-dir>/cache/downloads/<instance>/<plugin-id>/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Composite local assets should have an explicit reconstruction recipe checked in
|
||||||
|
with the helper. For example, Cinema's external Windows downloader tools are a
|
||||||
|
small deterministic bundle rebuilt from upstream executables with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PYTHONPATH=src python -m plugin_helper.recipes.cinema_tools --instance 1.44.1
|
||||||
|
```
|
||||||
|
|
||||||
Use `updates` to audit the version lock against obvious public update sources:
|
Use `updates` to audit the version lock against obvious public update sources:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -272,9 +272,9 @@ reason = "User-provided fork/branch URL https://github.com/MisterKelley/BeatSabe
|
|||||||
id = "cinema-tools"
|
id = "cinema-tools"
|
||||||
tag = "yt-dlp-2026.07.04-ffmpeg-8.1.2"
|
tag = "yt-dlp-2026.07.04-ffmpeg-8.1.2"
|
||||||
asset = "CinemaTools-yt-dlp-2026.07.04-ffmpeg-8.1.2.zip"
|
asset = "CinemaTools-yt-dlp-2026.07.04-ffmpeg-8.1.2.zip"
|
||||||
sha256 = "52a781344745d36381c58e0fafe790fefb6a2802df8f802e4835ba4443e1edab"
|
sha256 = "4d299e40f747abb9777838542ca68ec450e0b86e6eb11af341e6bd568f47edf2"
|
||||||
install_strategy = "root-zip"
|
install_strategy = "root-zip"
|
||||||
reason = "Helper-managed Windows executable bundle for Cinema's hardcoded Proton paths Libs/yt-dlp.exe and Libs/ffmpeg.exe. Built from yt-dlp/yt-dlp release 2026.07.04 asset yt-dlp.exe with upstream sha256 52fe3c26dcf71fbdc85b528589020bb0b8e383155cfa81b64dd447bbe35e24b8, plus GyanD/codexffmpeg release 8.1.2 asset ffmpeg-8.1.2-essentials_build.zip with upstream sha256 db580001caa24ac104c8cb856cd113a87b0a443f7bdf47d8c12b1d740584a2ec."
|
reason = "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 plugin_helper.recipes.cinema_tools --instance 1.44.1. Sources: https://github.com/yt-dlp/yt-dlp/releases/download/2026.07.04/yt-dlp.exe with sha256 52fe3c26dcf71fbdc85b528589020bb0b8e383155cfa81b64dd447bbe35e24b8, and https://github.com/GyanD/codexffmpeg/releases/download/8.1.2/ffmpeg-8.1.2-essentials_build.zip with sha256 db580001caa24ac104c8cb856cd113a87b0a443f7bdf47d8c12b1d740584a2ec; the recipe extracts */bin/ffmpeg.exe with sha256 1326dde4c84ff1f96fe6b8916c5bed29e163e9b5dccf995f6f3db069d143ec5e and writes a deterministic root zip."
|
||||||
|
|
||||||
[[plugins]]
|
[[plugins]]
|
||||||
id = "gottagofast"
|
id = "gottagofast"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Asset reconstruction recipes for helper-managed composite packages."""
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
from zipfile import ZIP_STORED, ZipFile, ZipInfo
|
||||||
|
|
||||||
|
|
||||||
|
PLUGIN_ID = "cinema-tools"
|
||||||
|
DEFAULT_INSTANCE = "1.44.1"
|
||||||
|
|
||||||
|
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_SHA256 = "52fe3c26dcf71fbdc85b528589020bb0b8e383155cfa81b64dd447bbe35e24b8"
|
||||||
|
|
||||||
|
FFMPEG_VERSION = "8.1.2"
|
||||||
|
FFMPEG_URL = (
|
||||||
|
f"https://github.com/GyanD/codexffmpeg/releases/download/{FFMPEG_VERSION}/"
|
||||||
|
f"ffmpeg-{FFMPEG_VERSION}-essentials_build.zip"
|
||||||
|
)
|
||||||
|
FFMPEG_ZIP_SHA256 = "db580001caa24ac104c8cb856cd113a87b0a443f7bdf47d8c12b1d740584a2ec"
|
||||||
|
FFMPEG_EXE_SHA256 = "1326dde4c84ff1f96fe6b8916c5bed29e163e9b5dccf995f6f3db069d143ec5e"
|
||||||
|
|
||||||
|
ASSET_NAME = f"CinemaTools-yt-dlp-{YT_DLP_VERSION}-ffmpeg-{FFMPEG_VERSION}.zip"
|
||||||
|
BUNDLE_SHA256 = "4d299e40f747abb9777838542ca68ec450e0b86e6eb11af341e6bd568f47edf2"
|
||||||
|
ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BuiltAsset:
|
||||||
|
path: Path
|
||||||
|
sha256: str
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
def _default_state_root() -> Path:
|
||||||
|
configured = os.environ.get("PLUGIN_HELPER_STATE_DIR")
|
||||||
|
if configured:
|
||||||
|
return Path(configured).expanduser()
|
||||||
|
xdg_state_home = os.environ.get("XDG_STATE_HOME")
|
||||||
|
if xdg_state_home:
|
||||||
|
return Path(xdg_state_home).expanduser() / "plugin-helper"
|
||||||
|
return Path("~/.local/state/plugin-helper").expanduser()
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256_file(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _download(url: str, destination: Path) -> None:
|
||||||
|
request = Request(url, headers={"User-Agent": "plugin-helper"})
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with urlopen(request, timeout=120) as response, destination.open("wb") as handle:
|
||||||
|
shutil.copyfileobj(response, handle)
|
||||||
|
|
||||||
|
|
||||||
|
def _verify(path: Path, expected_sha256: str, label: str) -> None:
|
||||||
|
actual = _sha256_file(path)
|
||||||
|
if actual != expected_sha256:
|
||||||
|
raise ValueError(f"{label} sha256 mismatch: expected {expected_sha256}, got {actual}")
|
||||||
|
|
||||||
|
|
||||||
|
def _read_ffmpeg_exe(archive_path: Path) -> bytes:
|
||||||
|
with ZipFile(archive_path) as archive:
|
||||||
|
matches = [
|
||||||
|
info
|
||||||
|
for info in archive.infolist()
|
||||||
|
if not info.is_dir() and info.filename.replace("\\", "/").endswith("/bin/ffmpeg.exe")
|
||||||
|
]
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise ValueError(f"expected exactly one ffmpeg.exe under */bin/, found {len(matches)}")
|
||||||
|
data = archive.read(matches[0])
|
||||||
|
actual = hashlib.sha256(data).hexdigest()
|
||||||
|
if actual != FFMPEG_EXE_SHA256:
|
||||||
|
raise ValueError(f"ffmpeg.exe sha256 mismatch: expected {FFMPEG_EXE_SHA256}, got {actual}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _write_member(archive: ZipFile, name: str, data: bytes) -> None:
|
||||||
|
info = ZipInfo(name, date_time=ZIP_TIMESTAMP)
|
||||||
|
info.compress_type = ZIP_STORED
|
||||||
|
info.external_attr = 0o644 << 16
|
||||||
|
archive.writestr(info, data)
|
||||||
|
|
||||||
|
|
||||||
|
def build_asset(output: Path, *, work_dir: Path | None = None) -> BuiltAsset:
|
||||||
|
output = output.expanduser()
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(dir=work_dir) as tmp:
|
||||||
|
tmp_path = Path(tmp)
|
||||||
|
yt_dlp_path = tmp_path / "yt-dlp.exe"
|
||||||
|
ffmpeg_zip_path = tmp_path / f"ffmpeg-{FFMPEG_VERSION}-essentials_build.zip"
|
||||||
|
staged_output = tmp_path / ASSET_NAME
|
||||||
|
|
||||||
|
_download(YT_DLP_URL, yt_dlp_path)
|
||||||
|
_verify(yt_dlp_path, YT_DLP_SHA256, "yt-dlp.exe")
|
||||||
|
|
||||||
|
_download(FFMPEG_URL, ffmpeg_zip_path)
|
||||||
|
_verify(ffmpeg_zip_path, FFMPEG_ZIP_SHA256, "ffmpeg essentials zip")
|
||||||
|
|
||||||
|
yt_dlp_data = yt_dlp_path.read_bytes()
|
||||||
|
ffmpeg_data = _read_ffmpeg_exe(ffmpeg_zip_path)
|
||||||
|
|
||||||
|
with ZipFile(staged_output, "w") as archive:
|
||||||
|
_write_member(archive, "Libs/yt-dlp.exe", yt_dlp_data)
|
||||||
|
_write_member(archive, "Libs/ffmpeg.exe", ffmpeg_data)
|
||||||
|
|
||||||
|
actual_bundle_sha256 = _sha256_file(staged_output)
|
||||||
|
if actual_bundle_sha256 != BUNDLE_SHA256:
|
||||||
|
raise ValueError(
|
||||||
|
f"{ASSET_NAME} sha256 mismatch: expected {BUNDLE_SHA256}, got {actual_bundle_sha256}"
|
||||||
|
)
|
||||||
|
shutil.move(str(staged_output), output)
|
||||||
|
|
||||||
|
return BuiltAsset(path=output, sha256=_sha256_file(output), size=output.stat().st_size)
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description="Rebuild the helper-managed Cinema tools bundle.")
|
||||||
|
parser.add_argument("--instance", default=DEFAULT_INSTANCE, help="Beat Saber instance/version cache key")
|
||||||
|
parser.add_argument(
|
||||||
|
"--state-dir",
|
||||||
|
type=Path,
|
||||||
|
default=_default_state_root(),
|
||||||
|
help="plugin-helper state root; defaults to PLUGIN_HELPER_STATE_DIR, XDG_STATE_HOME, or ~/.local/state/plugin-helper",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
type=Path,
|
||||||
|
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")
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
built = build_asset(output, work_dir=args.work_dir)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"error: {exc}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
print(f"Wrote: {built.path}")
|
||||||
|
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/ffmpeg.exe {FFMPEG_EXE_SHA256}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
raise SystemExit(run())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user