Preserve native broadcast video and add patient shared stream startup

This commit is contained in:
2026-10-25 16:59:56 +01:00
parent bd7395f045
commit 11a34a5195
27 changed files with 708 additions and 139 deletions
+58 -4
View File
@@ -12,14 +12,17 @@ import time
import urllib.error
import urllib.request
import uuid
import xml.etree.ElementTree as ET
VERSION = '7.0.14.0'
VERSION = '7.0.15.0'
ABI = '12.2.0.0'
GUID = '193f29f9-ea6c-4595-a6f6-55e79d7c590a'
DLL = 'Jellyfin.Plugin.Enigma2.dll'
PLUGINS = pathlib.Path('/var/lib/jellyfin/plugins')
BACKUPS = pathlib.Path('/var/lib/jellyfin/decoder-plugin-backups')
LOGS = pathlib.Path('/var/log/jellyfin')
WEB = pathlib.Path('/usr/share/jellyfin/web/decoder-channel-playback.js')
CONFIG = PLUGINS / 'configurations/Jellyfin.Plugin.Enigma2.xml'
PROTECTED = tuple(map(pathlib.Path, (
'/etc/default/jellyfin', '/etc/jellyfin/encoding.xml',
'/usr/local/lib/jellyfin-enigma/ffmpeg', '/usr/local/lib/jellyfin-enigma/ffprobe',
@@ -217,14 +220,40 @@ def activate(old, new, stage, backup, verify, verify_rollback, control=service):
shutil.rmtree(stage)
class PluginAssetControl:
"""Update only the plugin's assets while stopped; restore them on rollback."""
def __init__(self, old, new, assets, control=service):
self.old, self.new, self.assets, self.control = old, new, assets, control
def __call__(self, action):
if action == 'start':
candidate = self.new.exists() and not self.old.exists()
for destination, prepared, saved in self.assets:
shutil.copyfile(prepared if candidate else saved, destination)
self.control(action)
def enable_slates(content):
pattern = r'<EnableContinuitySlates>\s*(?:true|false)\s*</EnableContinuitySlates>'
tag = '<EnableContinuitySlates>true</EnableContinuitySlates>'
result = re.sub(pattern, tag, content) if re.search(pattern, content) else re.sub(r'(</[\w:]+>\s*)$', ' ' + tag + '\n' + r'\1', content, count=1)
if ET.fromstring(result).findtext('EnableContinuitySlates') != 'true':
raise ValueError('Cannot set the plugin status-picture option')
return result
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--maintenance-approved', action='store_true',
help='Required confirmation of an already agreed Jellyfin restart window')
parser.add_argument('--dll', type=pathlib.Path, required=True)
parser.add_argument('--expected-sha256', required=True)
parser.add_argument('--web', type=pathlib.Path)
parser.add_argument('--expected-web-sha256')
parser.add_argument('--enable-slates', action='store_true')
parser.add_argument('--check-only', action='store_true')
args = parser.parse_args(argv)
if not args.maintenance_approved:
if not args.maintenance_approved and not args.check_only:
parser.error('--maintenance-approved is required; this script cannot inspect playback sessions')
expected = args.expected_sha256.lower()
if not re.fullmatch(r'[0-9a-f]{64}', expected):
@@ -235,6 +264,16 @@ def main(argv=None):
new = PLUGINS / ('Enigma2_' + VERSION)
if new.exists() or new.is_symlink():
raise ValueError('Target plugin directory already exists; refusing to replace it')
if bool(args.web) != bool(args.expected_web_sha256):
raise ValueError('Web asset and its checksum must be supplied together')
if args.web and (not args.web.is_file() or sha256(args.web) != args.expected_web_sha256.lower() or not WEB.is_file()):
raise ValueError('Plugin web asset validation failed')
if args.enable_slates:
enable_slates(CONFIG.read_text(encoding='utf-8-sig'))
if args.check_only:
print(json.dumps({'preflight': True, 'activated': False, 'restart': False, 'candidate': VERSION,
'current': metadata['version'], 'web_included': bool(args.web), 'enable_slates_requested': args.enable_slates}))
return
before = hashes()
old_hash = sha256(old / DLL)
boundary = log_boundary(LOGS)
@@ -242,7 +281,16 @@ def main(argv=None):
backup = BACKUPS / ('plugin-only-' + stamp)
backup.mkdir(mode=0o700, parents=True, exist_ok=False)
stage = backup / 'candidate'
assets = []
try:
if args.web:
saved = backup / 'previous-web.js'; shutil.copy2(WEB, saved)
assets.append((WEB, args.web, saved))
if args.enable_slates:
saved = backup / 'previous-plugin-config.xml'; shutil.copy2(CONFIG, saved)
prepared = backup / 'candidate-plugin-config.xml'
prepared.write_text(enable_slates(CONFIG.read_text(encoding='utf-8-sig')), encoding='utf-8')
assets.append((CONFIG, prepared, saved))
shutil.copytree(old, stage, copy_function=shutil.copy2)
shutil.copy2(args.dll, stage / DLL)
updated = dict(metadata, version=VERSION, status='Active', targetAbi=ABI)
@@ -251,9 +299,15 @@ def main(argv=None):
shutil.chown(path, user='jellyfin', group='jellyfin')
if sha256(stage / DLL) != expected or hashes() != before:
raise RuntimeError('Pre-restart checksum verification failed')
def verify_plugin(directory, version, digest, candidate):
wait_ready(directory, version, digest, before, boundary)
for destination, prepared, saved in assets:
if sha256(destination) != sha256(prepared if candidate else saved):
raise RuntimeError('Plugin asset verification failed')
activate(old, new, stage, backup,
lambda: wait_ready(new, VERSION, expected, before, boundary),
lambda: wait_ready(old, metadata['version'], old_hash, before, boundary))
lambda: verify_plugin(new, VERSION, expected, True),
lambda: verify_plugin(old, metadata['version'], old_hash, False),
PluginAssetControl(old, new, assets))
finally:
if stage.exists():
shutil.rmtree(stage)