"""Install only the plugin DLL/manifest; --maintenance-approved permits the agreed restart.""" import argparse import datetime import hashlib import json import pathlib import shutil import sqlite3 import subprocess import time import urllib.error import urllib.request parser = argparse.ArgumentParser() parser.add_argument('--maintenance-approved', action='store_true') args = parser.parse_args() key = sqlite3.connect('file:/var/lib/jellyfin/data/jellyfin.db?mode=ro', uri=True).execute('SELECT AccessToken FROM ApiKeys LIMIT 1').fetchone()[0] headers = {'Authorization': 'MediaBrowser Token="' + key + '", Client="PluginDeployment", Device="Server", DeviceId="plugin-deployment", Version="7"'} sessions = json.load(urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:8096/Sessions', headers=headers))) if any(s.get('NowPlayingItem') for s in sessions) and not args.maintenance_approved: raise SystemExit('Active playback: maintenance approval required') protected = ['/etc/default/jellyfin', '/etc/jellyfin/encoding.xml', '/usr/local/lib/jellyfin-enigma/ffmpeg', '/usr/local/lib/jellyfin-enigma/ffprobe', '/usr/share/jellyfin/web/config.json', '/usr/share/jellyfin/web/index.html'] def hashes(): return {p: hashlib.sha256(pathlib.Path(p).read_bytes()).hexdigest() for p in protected if pathlib.Path(p).exists()} before = hashes() plugins = pathlib.Path('/var/lib/jellyfin/plugins') old = [p for p in plugins.glob('Enigma2_*') if p.is_dir()] backup = pathlib.Path('/var/lib/jellyfin/decoder-plugin-backups') / ('plugin-only-' + datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')) backup.mkdir(mode=0o700) new = plugins / 'Enigma2_7.0.7.0' metadata = json.loads((old[0] / 'meta.json').read_text()) metadata.update(version='7.0.7.0', status='Active', targetAbi='12.2.0.0') subprocess.run(['systemctl', 'stop', 'jellyfin'], check=True) try: for p in old: shutil.move(str(p), str(backup / p.name)) new.mkdir() shutil.copy2('/tmp/Jellyfin.Plugin.Enigma2.dll', new / 'Jellyfin.Plugin.Enigma2.dll') (new / 'meta.json').write_text(json.dumps(metadata)) subprocess.run(['chown', '-R', 'jellyfin:jellyfin', str(new)], check=True) except Exception: if new.exists(): shutil.rmtree(new) for p in old: if (backup / p.name).exists(): shutil.move(str(backup / p.name), str(p)) raise finally: subprocess.run(['systemctl', 'start', 'jellyfin'], check=True) assert hashes() == before, 'Unexpected change outside plugin installation' deadline = time.monotonic() + 45 while True: try: installed = json.load(urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:8096/Plugins', headers=headers), timeout=3)) if any(p['Name'] == 'Enigma2' and p['Version'] == '7.0.7.0' and p['Status'] == 'Active' for p in installed): break except (urllib.error.URLError, TimeoutError): pass if time.monotonic() >= deadline: raise RuntimeError('Jellyfin did not load the deployed plugin within 45 seconds; backup: ' + str(backup)) time.sleep(1) print(json.dumps({'plugin': '7.0.7.0', 'backup': str(backup), 'ready': True, 'server_configuration_and_wrappers_unchanged': True}))