Files
ProxMenux/oci/remote/configure_jellyfin_encoding.py
T
MacRimiandClaude Opus 5 bcabcb618c feat(oci): run official container images as native LXC containers
Adds the OCI manager: an engine that turns a Docker Compose file into an
LXC definition, a catalog of 365 applications drawn from LinuxServer.io
and other container image sources, and a per-instance registry recording
what each container was built from. Reachable from the main menu.

Catalog text is translated like every other string in the project: the
taglines go through translate() and land in lang/*.json, so the entries
read in all eight languages instead of only English.

Translation cache builder:
- a failed translation leaves the key absent rather than writing English,
  which previously made the string count as translated forever
- a result identical to a 3+ word source is rejected, catching a provider
  that silently returns the text it was given
- strings that are nothing but glossary terms keep their source spelling
  instead of being discarded as failures
- no backoff between attempts when the provider is deterministic
- application names are protected so "HAOS One" survives translation
- argos joins the provider list, and the workflow reads the OCI sources

Audit & Report:
- findings that moved in the wrong direction between runs are reported
  alongside the ones that improved
- an accepted risk can carry a review date and is flagged when it falls due
- backup checks explain in plain language what they looked at and what to
  do next

Monitor:
- disks can be excluded from periodic reads, and an idle disk says so
  instead of showing a stale temperature
- per-disk identity survives a controller or enclosure change
- scheduled Borg backups resolve their SSH key from the repository entry
- PVE upgrades log the package list and the resulting dpkg changes

The web build no longer copies scripts/ into public/: the documentation
links to GitHub, so nothing read that folder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 18:24:59 +02:00

102 lines
3.4 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import shutil
import sys
import tempfile
import xml.etree.ElementTree as ET
from pathlib import Path
from oci_ui import translate
def fail(message: str) -> None:
raise SystemExit(message)
def resolve_path(rootfs: Path, candidates: list[str]) -> Path:
rootfs = rootfs.resolve()
for candidate in candidates:
if not candidate.startswith("/") or "\x00" in candidate:
fail(f"{translate('Invalid configuration path:')} {candidate!r}")
resolved = (rootfs / candidate.lstrip("/")).resolve()
if rootfs not in resolved.parents:
fail(f"{translate('The path escapes the rootfs:')} {candidate}")
if resolved.is_file():
return resolved
fail(translate("Jellyfin has not created encoding.xml in any declared path"))
def update_encoding(rootfs: Path, configuration: dict[str, object]) -> tuple[Path, bool]:
path = resolve_path(rootfs, list(configuration.get("candidate_paths", [])))
tree = ET.parse(path)
root = tree.getroot()
changed = False
for tag, requested in dict(configuration.get("settings", {})).items():
if not isinstance(requested, str):
fail(f"{translate('The setting has no final value:')} {tag}")
element = root.find(tag)
if element is None:
element = ET.SubElement(root, tag)
changed = True
if (element.text or "") != requested:
element.text = requested
changed = True
for tag, requested_values in dict(configuration.get("lists", {})).items():
if not isinstance(requested_values, list) or not all(
isinstance(value, str) for value in requested_values
):
fail(f"{translate('Invalid list:')} {tag}")
element = root.find(tag)
if element is None:
element = ET.SubElement(root, tag)
changed = True
existing = [child.text or "" for child in list(element)]
if existing != requested_values:
for child in list(element):
element.remove(child)
for value in requested_values:
ET.SubElement(element, "string").text = value
changed = True
if not changed:
return path, False
backup = path.with_name(f"{path.name}.bak-proxmenux")
if not backup.exists():
shutil.copy2(path, backup)
os.chown(backup, path.stat().st_uid, path.stat().st_gid)
stat = path.stat()
fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
temporary = Path(temporary_name)
try:
with os.fdopen(fd, "wb") as stream:
tree.write(stream, encoding="utf-8", xml_declaration=True)
stream.flush()
os.fsync(stream.fileno())
os.chmod(temporary, stat.st_mode)
os.chown(temporary, stat.st_uid, stat.st_gid)
os.replace(temporary, path)
finally:
temporary.unlink(missing_ok=True)
return path, True
def main() -> None:
if len(sys.argv) != 3:
fail(f"{translate('Usage:')} configure_jellyfin_encoding.py ROOTFS CONFIGURATION_JSON")
rootfs = Path(sys.argv[1])
configuration = json.loads(sys.argv[2])
path, changed = update_encoding(rootfs, configuration)
state = "updated" if changed else "already applied"
print(f"Jellyfin configuration {state}: /{path.relative_to(rootfs.resolve())}")
if __name__ == "__main__":
main()