Files
Felitendo a16c2fea5a build: ship separate Jellyfin 10.11 and Jellyfin 12 artifacts
The Jellyfin 12 packages target net10.0 only, so one DLL cannot serve
both server generations. A JellyfinTarget switch (jf10/jf12) selects
the framework, the Jellyfin package version and a JF10/JF12 symbol.

Each release produces both artifacts with the same plugin version and
adds two manifest entries, Jellyfin 12 first. Jellyfin filters entries
by targetAbi before its stable sort by version, so manifest order
decides which build a Jellyfin 12 server picks. jprm repo add drops
entries sharing a version, so scripts/update_manifest.py writes them.

The 10.11 build is pinned to the 10.11.0 floor again, so the whole
10.11.x line can install it. CA1873 is relaxed for the .NET 10
analyzers, and CI builds both targets.
2026-09-15 15:45:00 +02:00

97 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Add one release to manifest.json as two entries - one per supported Jellyfin line.
`jprm repo add` cannot do this: update_plugin_manifest() drops any existing entry whose
version matches the one being added, so a second call for the same version would delete the
first artifact's entry instead of sitting next to it.
Two entries with the same version work because Jellyfin filters by ABI before it sorts:
.Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer)
...
foreach (var v in availableVersions.OrderByDescending(x => x.VersionNumber))
A 10.11 server never sees the 12.0.0.0 entry. A 12 server sees both, and since LINQ's
OrderByDescending is a stable sort and the caller takes the first result, the entry listed
first in the manifest wins the tie - hence Jellyfin 12 is written before Jellyfin 10.11.
"""
import argparse
import hashlib
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
BUILD_YAML = REPO_ROOT / "build.yaml"
MANIFEST = REPO_ROOT / "manifest.json"
# Order matters: the first matching entry wins on a server that can install both.
TARGET_ABIS = (("jf12", "12.0.0.0"), ("jf10", "10.11.0.0"))
def full_version(version):
"""Pad to four components; Jellyfin is not a fan of short version numbers."""
parts = str(version).split(".")
parts += ["0"] * (4 - len(parts))
return ".".join(parts[:4])
def md5(path):
digest = hashlib.md5() # noqa: S324 - manifest checksums are defined as md5 by Jellyfin
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
for key, _ in TARGET_ABIS:
parser.add_argument("--{}-zip".format(key), required=True, help="Built artifact for {}".format(key))
parser.add_argument("--{}-url".format(key), required=True, help="Public download URL for {}".format(key))
args = parser.parse_args()
build_cfg = yaml.safe_load(BUILD_YAML.read_text(encoding="utf-8"))
version = full_version(build_cfg["version"])
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
plugin = next((item for item in manifest if item.get("guid") == str(build_cfg["guid"]).lower()), None)
if plugin is None:
sys.exit("No plugin with guid {} in manifest.json".format(build_cfg["guid"]))
for field in ("name", "description", "overview", "owner", "category"):
plugin[field] = build_cfg[field]
kept = [entry for entry in plugin["versions"] if full_version(entry["version"]) != version]
dropped = len(plugin["versions"]) - len(kept)
if dropped:
print("Replacing {} existing entr{} for {}".format(dropped, "y" if dropped == 1 else "ies", version))
new_entries = []
for key, target_abi in TARGET_ABIS:
zip_path = Path(getattr(args, "{}_zip".format(key)))
if not zip_path.is_file():
sys.exit("Artifact not found: {}".format(zip_path))
new_entries.append({
"version": version,
"changelog": build_cfg["changelog"],
"targetAbi": target_abi,
"sourceUrl": getattr(args, "{}_url".format(key)),
"checksum": md5(zip_path),
"timestamp": timestamp,
})
print("{}: targetAbi {} -> {}".format(key, target_abi, zip_path.name))
plugin["versions"] = new_entries + kept
MANIFEST.write_text(json.dumps(manifest, indent=4, ensure_ascii=False) + "\n", encoding="utf-8")
print("manifest.json now has {} entries".format(len(plugin["versions"])))
if __name__ == "__main__":
main()