mirror of
https://github.com/MrSleeps/jellyfin-scene-search.git
synced 2026-11-14 22:11:27 +00:00
127 lines
5.7 KiB
Python
127 lines
5.7 KiB
Python
#!/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()
|