From e9314ea1ee04afa6bc9ce077ff740e2c5aaae748 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 2 Nov 2026 04:38:40 -0800 Subject: [PATCH] fix(jellyfin): anchor the provider-key write on the file's real root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TheTVDB writes its own configuration file and its serializer omits a null-valued setting entirely, so a fresh install has no element to fill, under a root element called . xml_secret's fallback insertion was hardcoded to , matched nothing, and install refused with "no place to write " — measured on Dankserver 2026-11-02, where the TVDB key has been present in the secret file since October and has never been installable. The anchor now comes from the document's own root, so both shapes Jellyfin actually produces work (the core plugins' and a plugin's ), and the refusal is kept for a file with no root at all. Tests: the plugin-written shape is red before this commit and green after; every other setting in the file is asserted preserved. --- scripts/jellyfin-providers.sh | 13 ++++++++++- tests/jellyfin_providers.bats | 41 ++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/scripts/jellyfin-providers.sh b/scripts/jellyfin-providers.sh index c982b65..9436e9d 100755 --- a/scripts/jellyfin-providers.sh +++ b/scripts/jellyfin-providers.sh @@ -135,7 +135,18 @@ text = escape(val) if pat.search(src): out = pat.sub(lambda m: f"{m.group(1)}<{elem}>{text}", src, count=1) else: - out = src.replace("", f" <{elem}>{text}\n", 1) + # The plugin owns this file and writes it under ITS OWN root element + # (TheTVDB's is ), and its serializer omits a + # null-valued setting entirely — so the element being filled can be + # absent outright, and a hardcoded anchor then matches + # nothing. Anchor on the document's real root instead. + out = src + root = re.search(r"<([A-Za-z_][\w.:-]*)[^>]*>", src) + if root and ("" % root.group(1)) in src: + name = root.group(1) + out = src.replace( + "" % name, f" <{elem}>{text}\n", 1 + ) if out == src: raise SystemExit(f"{xml_path}: no place to write <{elem}>") diff --git a/tests/jellyfin_providers.bats b/tests/jellyfin_providers.bats index c9e71d0..416da6e 100755 --- a/tests/jellyfin_providers.bats +++ b/tests/jellyfin_providers.bats @@ -13,9 +13,12 @@ # 3. a downloaded plugin artifact is accepted only on its pinned sha256, its # official guid and its pinned version (a swapped or downgraded artifact # is refused before anything lands in plugins/); -# 4. the TMDb key is written into the EXISTING config with every other -# setting preserved — Jellyfin owns that file, and clobbering it would -# silently reset country, language and adult settings; +# 4. a key is written into the EXISTING config with every other setting +# preserved, under whatever root that file's owner chose — Jellyfin's core +# configs say ``, TheTVDB writes `` and +# omits a null setting outright, so the element to fill can be absent and +# the anchor has to come from the file; clobbering the file would silently +# reset country, language and adult settings; # 5. a TVDB config the operator already saved through the UI is kept; # 6. install is idempotent: a second run changes no byte; # 7. whatever is replaced is copied to the trash first (fleet law: never @@ -254,6 +257,38 @@ XML grep -qF 'true' "$cfg" } +@test "the key lands in a plugin-written config with no ApiKey element" { + # What TheTVDB actually writes on a fresh install: its own root element and + # no at all, because its serializer drops null settings. The + # installer used to anchor on and refuse this file. + cat >"$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml" < + + + 1 + true + +XML + run prov install + [ "$status" -eq 0 ] + printf '%s\n' "${lines[@]}" | grep -qiF 'filled the ApiKey' + local cfg="$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml" + grep -qF "$TVDB_KEY" "$cfg" + grep -qF '1' "$cfg" + grep -qF 'true' "$cfg" +} + +@test "a config file with no root element is refused by name" { + # The guard still has to fire when there is genuinely nowhere to write — + # a refusal naming the file beats inventing a document shape. + printf '\n' \ + >"$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml" + run prov install + [ "$status" -ne 0 ] + printf '%s\n' "${lines[@]}" | grep -qF 'Jellyfin.Plugin.Tvdb.xml' + printf '%s\n' "${lines[@]}" | grep -qiF 'no place to write' +} + @test "a pin in the TVDB secret lands in the plugin config" { # A subscriber-shaped key needs the account's PIN beside it; a licensed key # does not, so the element appears only when the secret carries one. -- 2.54.0