#!/usr/bin/env python3 """Add a version entry to the Jellyfin plugin repository manifest. manifest.json is what Jellyfin fetches when the repository URL is added under Dashboard -> Plugins -> Repositories. Plugin metadata comes from build.yaml so there is a single source of truth. Usage: scripts/update_manifest.py --version 1.0.4.0 --checksum --source-url """ import argparse import datetime import json import re import sys from pathlib import Path import yaml REPO_ROOT = Path(__file__).resolve().parent.parent BUILD_YAML = REPO_ROOT / "build.yaml" MANIFEST = REPO_ROOT / "manifest.json" def version_key(version): return tuple(int(part) for part in re.findall(r"\d+", version)) def load_manifest(): if not MANIFEST.is_file(): return [] with MANIFEST.open(encoding="utf-8") as handle: return json.load(handle) def find_plugin(manifest, guid): for plugin in manifest: if plugin.get("guid", "").lower() == guid.lower(): return plugin return None def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--version", required=True) parser.add_argument("--checksum", required=True, help="md5 of the zip") parser.add_argument("--source-url", required=True, help="download URL for the zip") parser.add_argument("--changelog", default="") parser.add_argument("--timestamp", default="", help="ISO 8601 UTC; defaults to now") args = parser.parse_args() with BUILD_YAML.open(encoding="utf-8") as handle: cfg = yaml.safe_load(handle) timestamp = args.timestamp or ( datetime.datetime.now(datetime.timezone.utc) .replace(tzinfo=None) .isoformat(timespec="seconds") + "Z" ) manifest = load_manifest() plugin = find_plugin(manifest, cfg["guid"]) if plugin is None: plugin = {"guid": cfg["guid"], "versions": []} manifest.append(plugin) # Refresh the descriptive fields so build.yaml stays authoritative. plugin["name"] = cfg["name"] plugin["description"] = cfg["description"].strip() plugin["overview"] = cfg["overview"].strip() plugin["owner"] = cfg["owner"] plugin["category"] = cfg["category"] entry = { "version": args.version, "changelog": (args.changelog or cfg.get("changelog", "")).strip(), "targetAbi": cfg["targetAbi"], "sourceUrl": args.source_url, "checksum": args.checksum, "timestamp": timestamp, } versions = [v for v in plugin.get("versions", []) if v.get("version") != args.version] versions.append(entry) versions.sort(key=lambda v: version_key(v["version"]), reverse=True) plugin["versions"] = versions # Key order matters only for readability; Jellyfin reads by name. ordered = [ { key: plugin[key] for key in ( "guid", "name", "description", "overview", "owner", "category", "versions", ) if key in plugin } for plugin in manifest ] MANIFEST.write_text(json.dumps(ordered, indent=4) + "\n", encoding="utf-8") print(f"manifest.json now lists {args.version} at {args.source_url}") if __name__ == "__main__": sys.exit(main())