- Multi-stage Dockerfile: discover patterns from PMS binary (capstone), compile .so with zig (musl), layer onto lscr.io/linuxserver/plex - Uses LD_PRELOAD instead of patchelf (which corrupts Plex's musl loader) - Auto-discovery: broad structural patterns with string-anchored fallback (//feature) and relationship-based fallback (BITSET_REF within BS_INIT) - hook.cpp uses __has_include for generated patterns with hardcoded fallbacks - Custom wrapper.sh (no traffic_logger preload) - Vendored Freeloader source (github.com/authrequest/Freeloader, AGPL-3.0) - Removed stale plexmediaserver_crack.so binary - Supports Plex 1.43.3+ (verified against 1.43.2 and 1.43.3)
115 lines
3.8 KiB
Python
115 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""Read and edit Plex ``Preferences.xml`` attributes.
|
|
|
|
Used by ``plex-tailscale-setup.sh``. The XML logic lives here -- not in a bash
|
|
heredoc -- so it is cohesive, reviewable, and independently testable. The shell
|
|
owns the lifecycle (stop Plex, back up, restore ownership, restart); this owns
|
|
the document.
|
|
|
|
plex_prefs.py merge PREFS [--custom-url URL] [--lan CIDR[,CIDR...]]
|
|
[--secure 0|1|2] [--relay 0|1]
|
|
plex_prefs.py get PREFS ATTR
|
|
|
|
``merge`` is additive and idempotent: list attributes gain only missing values;
|
|
scalar attributes are set only when a value is supplied. Unrelated attributes
|
|
(tokens, machine identity, ...) are preserved.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from collections.abc import Callable
|
|
from typing import Protocol, cast
|
|
|
|
try:
|
|
import defusedxml.ElementTree as ET
|
|
except ModuleNotFoundError:
|
|
sys.exit("plex_prefs: missing dependency: install python3-defusedxml")
|
|
|
|
|
|
class _PrefsElement(Protocol):
|
|
tag: str
|
|
|
|
def get(self, key: str, default: str = "") -> str: ...
|
|
def set(self, key: str, value: str) -> None: ...
|
|
|
|
|
|
class _PrefsTree(Protocol):
|
|
def getroot(self) -> _PrefsElement: ...
|
|
def write(self, file_or_filename: str, encoding: str, xml_declaration: bool) -> None: ...
|
|
|
|
|
|
def _load(path: str) -> tuple[_PrefsTree, _PrefsElement]:
|
|
try:
|
|
tree = cast(_PrefsTree, cast(object, ET.parse(path)))
|
|
except (OSError, ET.ParseError) as exc:
|
|
sys.exit(f"plex_prefs: cannot read {path}: {exc}")
|
|
root = tree.getroot()
|
|
if root.tag != "Preferences":
|
|
sys.exit(f"plex_prefs: unexpected root <{root.tag}>; refusing to edit {path}")
|
|
return tree, root
|
|
|
|
|
|
def _merge_csv(root: _PrefsElement, attr: str, additions: list[str]) -> None:
|
|
items = [x for x in (s.strip() for s in root.get(attr, "").split(",")) if x]
|
|
for value in additions:
|
|
if value and value not in items:
|
|
items.append(value)
|
|
root.set(attr, ",".join(items))
|
|
|
|
|
|
def cmd_merge(args: argparse.Namespace) -> int:
|
|
prefs = cast(str, args.prefs)
|
|
custom_url = cast(str, args.custom_url)
|
|
lan = cast(str, args.lan)
|
|
secure = cast(str, args.secure)
|
|
relay = cast(str, args.relay)
|
|
|
|
tree, root = _load(prefs)
|
|
if custom_url:
|
|
_merge_csv(root, "customConnections", [custom_url])
|
|
if lan:
|
|
_merge_csv(root, "LanNetworksBandwidth", [c for c in lan.split(",") if c])
|
|
if secure in ("0", "1", "2"):
|
|
root.set("secureConnections", secure)
|
|
if relay in ("0", "1"):
|
|
root.set("RelayEnabled", relay)
|
|
tree.write(prefs, encoding="utf-8", xml_declaration=True)
|
|
return 0
|
|
|
|
|
|
def cmd_get(args: argparse.Namespace) -> int:
|
|
prefs = cast(str, args.prefs)
|
|
attr = cast(str, args.attr)
|
|
|
|
_, root = _load(prefs)
|
|
print(root.get(attr, ""))
|
|
return 0
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(prog="plex_prefs", description=__doc__)
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
|
|
m = sub.add_parser("merge", help="merge tailnet settings into Preferences.xml")
|
|
_ = m.add_argument("prefs")
|
|
_ = m.add_argument("--custom-url", default="")
|
|
_ = m.add_argument("--lan", default="")
|
|
_ = m.add_argument("--secure", default="", help="0=Required 1=Preferred 2=Disabled")
|
|
_ = m.add_argument("--relay", default="", help="0=disable 1=enable Plex Relay")
|
|
m.set_defaults(func=cmd_merge)
|
|
|
|
g = sub.add_parser("get", help="print one Preferences.xml attribute")
|
|
_ = g.add_argument("prefs")
|
|
_ = g.add_argument("attr")
|
|
g.set_defaults(func=cmd_get)
|
|
|
|
args = parser.parse_args(argv)
|
|
func = cast(Callable[[argparse.Namespace], int], args.func)
|
|
return func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|