Add Gitea release packager and Jellyfin catalog publisher

This commit is contained in:
Sleeps
2026-10-15 16:52:03 +01:00
parent b0749917c3
commit c1aaf63c41
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Package Scene Search for the Jellyfin catalog and optionally publish to Gitea.
Requires only Python stdlib. Run from repository root after a Release build.
"""
import argparse
import base64
import datetime as dt
import hashlib
import json
import os
from pathlib import Path
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
import zipfile
ROOT = Path(__file__).resolve().parents[1]
PROJECT = ROOT / "jellyfin-plugin/Jellyfin.Plugin.SceneSearch/Jellyfin.Plugin.SceneSearch.csproj"
DLL = ROOT / "jellyfin-plugin/Jellyfin.Plugin.SceneSearch/bin/Release/net10.0/Jellyfin.Plugin.SceneSearch.dll"
GUID = "2d8d1bb5-3f0c-4d5a-9d59-934463c06d35"
def request(base, token, method, route, payload=None, content_type="application/json"):
url = base.rstrip("/") + "/api/v1" + route
data = None
if payload is not None:
data = json.dumps(payload).encode() if content_type == "application/json" else payload
headers = {"Authorization": "token " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = content_type
req = urllib.request.Request(url, data=data, method=method, headers=headers)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read()
return json.loads(raw) if raw else {}
except urllib.error.HTTPError as error:
if error.code == 404:
return None
raise RuntimeError(f"{method} {route}: HTTP {error.code}: {error.read()[:500]!r}") from error
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--publish", action="store_true", help="Publish to a separate public Gitea distribution repository")
args = parser.parse_args()
project = PROJECT.read_text()
m = re.search(r"<Version>([^<]+)</Version>", project)
if not m:
sys.exit("Missing project Version")
version = m.group(1)
tag = os.environ.get("RELEASE_TAG", "v" + version)
if tag != "v" + version:
sys.exit(f"Release tag {tag!r} does not match project version v{version}")
if not DLL.is_file():
sys.exit(f"Build DLL first: {DLL}")
dist = ROOT / "dist"
dist.mkdir(exist_ok=True)
archive = dist / f"SceneSearch-{version}.zip"
with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as z:
z.write(DLL, "Jellyfin.Plugin.SceneSearch.dll")
checksum = hashlib.md5(archive.read_bytes()).hexdigest() # Jellyfin catalog schema requires MD5.
print(f"Packaged {archive} (MD5 {checksum})")
if not args.publish:
return
base = os.environ["GITEA_URL"].rstrip("/")
repo = os.environ["GITEA_REPO"].strip("/")
token = os.environ["GITEA_TOKEN"]
quoted_repo = "/repos/" + "/".join(urllib.parse.quote(x, safe="") for x in repo.split("/"))
info = request(base, token, "GET", quoted_repo)
if not info:
sys.exit("Gitea distribution repository does not exist")
if info.get("private"):
sys.exit("Refusing to publish catalog into a private Gitea repository")
release_route = quoted_repo + "/releases/tags/" + urllib.parse.quote(tag, safe="")
release = request(base, token, "GET", release_route)
if release is None:
release = request(base, token, "POST", quoted_repo + "/releases", {
"tag_name": tag, "name": f"Scene Search {version}",
"body": f"Jellyfin Scene Search plugin {version}. Requires the separate Scene Search service.",
"draft": False, "prerelease": False,
})
assets = release.get("assets", [])
name = archive.name
asset = next((a for a in assets if a.get("name") == name), None)
if asset is None:
route = quoted_repo + f"/releases/{release['id']}/assets?name=" + urllib.parse.quote(name)
asset = request(base, token, "POST", route, archive.read_bytes(), "application/octet-stream")
url = asset.get("browser_download_url")
if not url or not url.startswith(base + "/"):
sys.exit("Gitea did not supply a valid public release URL")
manifest_route = quoted_repo + "/contents/manifest.json"
existing = request(base, token, "GET", manifest_route)
if existing:
catalog = json.loads(base64.b64decode(existing["content"]).decode())
else:
catalog = []
entries = [x for x in catalog if x.get("guid", "").lower() == GUID]
if len(entries) > 1:
sys.exit("Duplicate plugin GUID in distribution manifest")
plugin = entries[0] if entries else {
"guid": GUID, "name": "Scene Search",
"description": "Find scenes within your Jellyfin libraries using semantic search.",
"overview": "Jellyfin integration for the separately hosted Scene Search service.",
"owner": "MrSleeps", "category": "General", "versions": [],
}
record = {
"version": version, "changelog": f"Scene Search {version}",
"targetAbi": "12.0.0.0", "sourceUrl": url, "checksum": checksum,
"timestamp": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),
}
plugin["versions"] = [record] + [v for v in plugin["versions"] if v["version"] != version]
if not entries:
catalog.append(plugin)
content = base64.b64encode((json.dumps(catalog, indent=2) + "\n").encode()).decode()
body = {"content": content, "message": f"Publish Scene Search {version}"}
if existing:
body["sha"] = existing["sha"]
result = request(base, token, "PUT" if existing else "POST", manifest_route, body)
if not result:
sys.exit("Failed to update manifest")
print(f"Published release {url}")
print(f"Jellyfin repository: {base}/{repo}/raw/branch/{info.get('default_branch', 'main')}/manifest.json")
if __name__ == "__main__":
main()