feat(jellyfin): the provider keys ride a host file the deployer installs
Jellyfin 12 ships TMDB, TVDB, StudioImages and MusicBrainz as core plugins: no DLL, only a configuration file under the data root, so the keys have to reach that XML from somewhere that is not git. They live in the fleet's per-service env-file convention -- /home/dank/Containers/jellyfin/provider-keys (TMDB_API_KEY=, TVDB_API_KEY=, optional TVDB_PIN=, mode 600) -- and scripts/jellyfin-providers.sh plan|install|verify reads only that file, prints no key byte, and refuses a keys file that is not mode 600 or that has no line for the provider about to be written. install is idempotent: it never touches a key the UI already saved, backs up every XML it edits into ~/.AGENTTRASH/<date>-jellyfin-providers/ first, and only installs the TVDB plugin when the pinned official zip carries the plugin guid. The deployer runs install BEFORE it restarts jellyfin, because the plugin and its keys are read at start-up and a restart-first deploy boots the old state. A missing keys file warns instead of failing: a host without the keys still deploys, degraded rather than broken. No plugin configuration XML is tracked.
This commit is contained in:
1 parent
38737ab84e
commit
3ce8d62e70
4 files changed
+713
No files matched your search
@@ -476,6 +476,11 @@ def services_for_path(path: str, target: str) -> set[str]:
|
|||||||
# The installer is what creates, mounts and reaps the view both media
|
# The installer is what creates, mounts and reaps the view both media
|
||||||
# servers read, so a change to it is a restart of that pair.
|
# servers read, so a change to it is a restart of that pair.
|
||||||
_add(services, "jellyfin", "plex")
|
_add(services, "jellyfin", "plex")
|
||||||
|
if name == "jellyfin-providers.sh":
|
||||||
|
# The deployer runs this before jellyfin restarts, so a change to
|
||||||
|
# the installer is a change to the provider state Jellyfin loads at
|
||||||
|
# start-up: it selects jellyfin and nothing else.
|
||||||
|
_add(services, "jellyfin")
|
||||||
if name in {"frank-zealot.env", "frank-zealot.service", "serve-frank-zealot.sh"}:
|
if name in {"frank-zealot.env", "frank-zealot.service", "serve-frank-zealot.sh"}:
|
||||||
# llama.cpp frank-zealot (2026-09-16) keeps its unit, env and serve
|
# llama.cpp frank-zealot (2026-09-16) keeps its unit, env and serve
|
||||||
# files here; each one selects frank-zealot only. The ik_llama.cpp
|
# files here; each one selects frank-zealot only. The ik_llama.cpp
|
||||||
|
|||||||
@@ -1216,8 +1216,28 @@ ensure_media_view() {
|
|||||||
bash scripts/media-view.sh verify
|
bash scripts/media-view.sh verify
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_provider_keys() {
|
||||||
|
# Jellyfin's metadata-provider keys live in the container-owned plugin
|
||||||
|
# configuration under the data root, never in git; scripts/jellyfin-providers.sh
|
||||||
|
# is what places them, reading the per-service host env file (the fleet
|
||||||
|
# convention: grafana, caddy, pihole, copyparty, romm, seerr) that holds
|
||||||
|
# TMDB_API_KEY= and TVDB_API_KEY=, mode 600.
|
||||||
|
# Missing keys mean a keyless provider — a degraded library, not a broken
|
||||||
|
# deploy — so this warns rather than failing the restart. But when the keys
|
||||||
|
# ARE placed it must happen before the restart: the plugin and its keys are
|
||||||
|
# read at start-up, so a restart first would boot the old state.
|
||||||
|
local keys=/home/dank/Containers/jellyfin/provider-keys
|
||||||
|
if [ -f "$keys" ]; then
|
||||||
|
bash scripts/jellyfin-providers.sh install
|
||||||
|
else
|
||||||
|
echo "WARN: $keys is absent - provider keys NOT placed"
|
||||||
|
echo "WARN: create it (TMDB_API_KEY=, TVDB_API_KEY=, chmod 600) and re-deploy jellyfin"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
deploy_jellyfin() {
|
deploy_jellyfin() {
|
||||||
ensure_media_view
|
ensure_media_view
|
||||||
|
ensure_provider_keys
|
||||||
cp "dankserver/jellyfin/quadlet/jellyfin.container" "$quadlet_unit_dir/"
|
cp "dankserver/jellyfin/quadlet/jellyfin.container" "$quadlet_unit_dir/"
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
if ! systemctl --user is-active -q jellyfin.service; then
|
if ! systemctl --user is-active -q jellyfin.service; then
|
||||||
|
|||||||
Executable
+391
@@ -0,0 +1,391 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Metadata-provider keys for Jellyfin on Dankserver, placed from the service's
|
||||||
|
# host env file with the plugin artifact pinned by content hash.
|
||||||
|
#
|
||||||
|
# Jellyfin 12 ships TMDb, OMDb, StudioImages and MusicBrainz as CORE plugins:
|
||||||
|
# they have no DLL, only a configuration file under the data root, and that
|
||||||
|
# file is created by the running container. So the fleet's provider state lives
|
||||||
|
# in two places the repo does not track —
|
||||||
|
#
|
||||||
|
# plugins/configurations/Jellyfin.Plugin.Tmdb.xml <TmdbApiKey /> (empty = keyless)
|
||||||
|
# plugins/configurations/Jellyfin.Plugin.Tvdb.xml <ApiKey /> (needs the plugin)
|
||||||
|
#
|
||||||
|
# and both are owned by the container user. This tool is the only thing that
|
||||||
|
# writes them: it is declarative, idempotent, and it never prints a key, so it
|
||||||
|
# is safe to run while someone watches.
|
||||||
|
#
|
||||||
|
# Keys never enter git and never enter argv (a value in argv is world-readable
|
||||||
|
# through /proc/<pid>/cmdline). They live in the per-service host env file that
|
||||||
|
# is the fleet's convention for container credentials (grafana, caddy, pihole,
|
||||||
|
# copyparty, romm, seerr), mode 600:
|
||||||
|
#
|
||||||
|
# /home/dank/Containers/jellyfin/provider-keys
|
||||||
|
# TMDB_API_KEY=<key>
|
||||||
|
# TVDB_API_KEY=<key>
|
||||||
|
# TVDB_PIN=<pin> # optional: only a subscriber-shaped TVDB key
|
||||||
|
# # needs one; a licensed key does not
|
||||||
|
#
|
||||||
|
# TVDB itself is not core in 12 (the metadata provider was cut in 10.9), so it
|
||||||
|
# is installed from the official catalog, pinned three ways: sha256 of the
|
||||||
|
# artifact, the plugin's own guid, and its version. A matching sha proves only
|
||||||
|
# that the bytes are the bytes fetched — the guid check is what says it is the
|
||||||
|
# official plugin, the version check is what says it is the one this file was
|
||||||
|
# reviewed against.
|
||||||
|
#
|
||||||
|
# Subcommands
|
||||||
|
# plan print the resolved shape and every pending change, touch nothing
|
||||||
|
# install fetch+verify the plugin, then write the keys (backing up first)
|
||||||
|
# verify prove the plugin and both keys are actually in place
|
||||||
|
#
|
||||||
|
# Nothing here restarts Jellyfin: the deployer owns that
|
||||||
|
# (`scripts/deploy.sh dankserver --service jellyfin`), and the plugin is read at
|
||||||
|
# start-up, so a key written while the server runs is not live until then.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Jellyfin's data root on Dankserver (the rootless quadlet's config volume).
|
||||||
|
JF_ROOT="${JF_ROOT:-/home/dank/Containers/jellyfin/config}"
|
||||||
|
PROV_KEYS="${PROV_KEYS:-/home/dank/Containers/jellyfin/provider-keys}"
|
||||||
|
TRASH_DIR="${TRASH_DIR:-$HOME/.AGENTTRASH/$(date +%Y%m%d)-jellyfin-providers}"
|
||||||
|
|
||||||
|
PLUGINS="$JF_ROOT/plugins"
|
||||||
|
CONF="$PLUGINS/configurations"
|
||||||
|
TMDB_XML="$CONF/Jellyfin.Plugin.Tmdb.xml"
|
||||||
|
TVDB_XML="$CONF/Jellyfin.Plugin.Tvdb.xml"
|
||||||
|
|
||||||
|
# repo.jellyfin.org catalog entry for TheTVDB (owner: jellyfin), version 24.0.0.0,
|
||||||
|
# targetAbi 12.0.0.0 — published 2026-09-09, verified against Jellyfin 12.4.0.
|
||||||
|
TVDB_PLUGIN_URL="${TVDB_PLUGIN_URL:-https://repo.jellyfin.org/files/plugin/thetvdb/thetvdb_24.0.0.0.zip}"
|
||||||
|
TVDB_PLUGIN_SHA256="${TVDB_PLUGIN_SHA256:-6302fba3857bdde8d186dd099fac09ca43247041f531a528a2c788add70a5319}"
|
||||||
|
TVDB_PLUGIN_GUID="${TVDB_PLUGIN_GUID:-a677c0da-fac5-4cde-941a-7134223f14c8}"
|
||||||
|
TVDB_PLUGIN_VERSION="${TVDB_PLUGIN_VERSION:-24.0.0.0}"
|
||||||
|
PLUGIN_DIR="thetvdb_${TVDB_PLUGIN_VERSION}"
|
||||||
|
TVDB_DLL="Jellyfin.Plugin.Tvdb.dll"
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf 'jellyfin-providers: %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
printf 'usage: %s plan|install|verify\n' "$0" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# check_keys — refuse anything that is not a readable private file. An open
|
||||||
|
# mode is an error, not a warning: a provider key readable by every user on the
|
||||||
|
# box is the failure this check exists to catch.
|
||||||
|
check_keys() {
|
||||||
|
[ -f "$PROV_KEYS" ] ||
|
||||||
|
die "$PROV_KEYS not found (create it with TMDB_API_KEY= and TVDB_API_KEY=, then chmod 600)"
|
||||||
|
local mode
|
||||||
|
mode="$(stat -c '%a' "$PROV_KEYS")"
|
||||||
|
case "$mode" in
|
||||||
|
600 | 400) ;;
|
||||||
|
*) die "$PROV_KEYS is mode $mode; provider keys must be 600" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# keys_value <VAR> — the value of one variable in the env file, or nothing.
|
||||||
|
keys_value() {
|
||||||
|
awk -v k="$1" -F= '$1 == k { sub(/^[^=]*=/, ""); print; exit }' "$PROV_KEYS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# keys_len <VAR> — the length, and a refusal naming the variable when absent.
|
||||||
|
# The value itself is never printed.
|
||||||
|
keys_len() {
|
||||||
|
local v
|
||||||
|
v="$(keys_value "$1")"
|
||||||
|
[ -n "$v" ] || die "$PROV_KEYS has no $1= line"
|
||||||
|
printf '%s' "${#v}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# xml_value <file> <element> — the element's text, empty when it is absent or
|
||||||
|
# self-closing.
|
||||||
|
xml_value() {
|
||||||
|
python3 - "$1" "$2" <<'PYEOF'
|
||||||
|
import re, sys
|
||||||
|
src = open(sys.argv[1], encoding="utf-8").read()
|
||||||
|
m = re.search(r"<%s(?:\s*/>|>(.*?)</%s>)" % (sys.argv[2], sys.argv[2]), src, re.S)
|
||||||
|
print(m.group(1).strip() if (m and m.group(1)) else "")
|
||||||
|
PYEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# xml_secret <file> <element> <var> — set <element> to that variable from the
|
||||||
|
# env file. The python process reads the env file itself, so the value never
|
||||||
|
# appears in argv.
|
||||||
|
xml_secret() {
|
||||||
|
python3 - "$1" "$2" "$3" "$PROV_KEYS" <<'PYEOF'
|
||||||
|
import os, re, sys, tempfile
|
||||||
|
from xml.sax.saxutils import escape
|
||||||
|
|
||||||
|
xml_path, elem, var, keys_path = sys.argv[1:5]
|
||||||
|
val = ""
|
||||||
|
for line in open(keys_path, encoding="utf-8"):
|
||||||
|
key, sep, rest = line.partition("=")
|
||||||
|
if sep and key.strip() == var:
|
||||||
|
val = rest.strip()
|
||||||
|
break
|
||||||
|
if not val:
|
||||||
|
raise SystemExit(f"{keys_path} has no {var}= line")
|
||||||
|
|
||||||
|
src = open(xml_path, encoding="utf-8").read()
|
||||||
|
pat = re.compile(r"([ \t]*)<%s(?:\s*/>|>.*?</%s>)" % (elem, elem), re.S)
|
||||||
|
text = escape(val)
|
||||||
|
if pat.search(src):
|
||||||
|
out = pat.sub(lambda m: f"{m.group(1)}<{elem}>{text}</{elem}>", src, count=1)
|
||||||
|
else:
|
||||||
|
out = src.replace("</Configuration>", f" <{elem}>{text}</{elem}>\n</Configuration>", 1)
|
||||||
|
if out == src:
|
||||||
|
raise SystemExit(f"{xml_path}: no place to write <{elem}>")
|
||||||
|
|
||||||
|
d, base = os.path.split(xml_path)
|
||||||
|
fd, tmp = tempfile.mkstemp(prefix=base + ".", dir=d)
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(out)
|
||||||
|
os.chmod(tmp, os.stat(xml_path).st_mode & 0o777)
|
||||||
|
os.replace(tmp, xml_path)
|
||||||
|
PYEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# plugin_meta <zip> — "guid version" out of the artifact's own meta.json.
|
||||||
|
plugin_meta() {
|
||||||
|
python3 - "$1" <<'PYEOF'
|
||||||
|
import json, sys, zipfile
|
||||||
|
meta = json.loads(zipfile.ZipFile(sys.argv[1]).read("meta.json").decode("utf-8"))
|
||||||
|
print(meta.get("guid", ""), meta.get("version", ""))
|
||||||
|
PYEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
check_root() {
|
||||||
|
# Never fabricate a data root: creating plugins/ beside a typo'd path would
|
||||||
|
# install a plugin Jellyfin never loads and hide the operator's mistake.
|
||||||
|
[ -d "$CONF" ] || die "no Jellyfin configuration directory at $CONF (is JF_ROOT right?)"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_keys_report() {
|
||||||
|
check_keys
|
||||||
|
# A refusal inside a command substitution used as a printf argument would
|
||||||
|
# be swallowed, so the guard runs as its own statement.
|
||||||
|
keys_len TMDB_API_KEY >/dev/null
|
||||||
|
keys_len TVDB_API_KEY >/dev/null
|
||||||
|
printf '= keys read from %s (mode %s)\n' "$PROV_KEYS" "$(stat -c '%a' "$PROV_KEYS")"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The plugin's state, as one line in the shared vocabulary.
|
||||||
|
plugin_state() {
|
||||||
|
if [ -d "$PLUGINS/$PLUGIN_DIR" ]; then
|
||||||
|
printf '= %s already installed\n' "$PLUGIN_DIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local other
|
||||||
|
other="$(find "$PLUGINS" -maxdepth 1 -type d -name 'thetvdb_*' ! -name "$PLUGIN_DIR" -printf '%f\n' 2>/dev/null | sort | tr '\n' ' ')"
|
||||||
|
if [ -n "$other" ]; then
|
||||||
|
printf '+ install %s (a different version is present: %s)\n' "$PLUGIN_DIR" "$other"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '+ install %s from %s\n' "$PLUGIN_DIR" "${TVDB_PLUGIN_URL#*://}"
|
||||||
|
}
|
||||||
|
|
||||||
|
tmdb_state() {
|
||||||
|
if [ ! -f "$TMDB_XML" ]; then
|
||||||
|
printf '! %s is missing — save the TMDb plugin settings once in the UI, then re-run\n' "$TMDB_XML" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local now want
|
||||||
|
now="$(xml_value "$TMDB_XML" TmdbApiKey)"
|
||||||
|
want="$(keys_value TMDB_API_KEY)"
|
||||||
|
if [ -n "$now" ] && [ "$now" = "$want" ]; then
|
||||||
|
printf '= TmdbApiKey already set (%s chars)\n' "${#now}"
|
||||||
|
elif [ -n "$now" ]; then
|
||||||
|
printf '+ replace the TmdbApiKey in Jellyfin.Plugin.Tmdb.xml\n'
|
||||||
|
else
|
||||||
|
printf '+ set TmdbApiKey in Jellyfin.Plugin.Tmdb.xml\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tvdb_state() {
|
||||||
|
local now
|
||||||
|
if [ -f "$TVDB_XML" ]; then
|
||||||
|
now="$(xml_value "$TVDB_XML" ApiKey)"
|
||||||
|
if [ -n "$now" ]; then
|
||||||
|
printf '= Jellyfin.Plugin.Tvdb.xml already has an ApiKey (kept)\n'
|
||||||
|
else
|
||||||
|
printf '+ fill the empty ApiKey in Jellyfin.Plugin.Tvdb.xml\n'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '+ create Jellyfin.Plugin.Tvdb.xml with ApiKey\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_plugin() {
|
||||||
|
local tmp guid version other
|
||||||
|
other="$(find "$PLUGINS" -maxdepth 1 -type d -name 'thetvdb_*' ! -name "$PLUGIN_DIR" -printf '%f\n' 2>/dev/null | sort | tr '\n' ' ')"
|
||||||
|
[ -z "$other" ] ||
|
||||||
|
die "another TheTVDB version is already installed ($other); move it to the trash first — two copies of one plugin means two loads"
|
||||||
|
|
||||||
|
tmp="$(mktemp "${TMPDIR:-/tmp}/thetvdb.XXXXXX.zip")"
|
||||||
|
# The pins are checked before anything is unpacked, so a bad artifact never
|
||||||
|
# reaches the plugins directory.
|
||||||
|
curl -fsSL --max-time 300 -o "$tmp" "$TVDB_PLUGIN_URL" ||
|
||||||
|
{ rm -f "$tmp"; die "could not fetch $TVDB_PLUGIN_URL"; }
|
||||||
|
local got
|
||||||
|
got="$(sha256sum "$tmp" | cut -d' ' -f1)"
|
||||||
|
if [ "$got" != "$TVDB_PLUGIN_SHA256" ]; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
die "artifact sha256 is $got, pinned $TVDB_PLUGIN_SHA256 — refusing to install"
|
||||||
|
fi
|
||||||
|
read -r guid version <<<"$(plugin_meta "$tmp")"
|
||||||
|
if [ "$guid" != "$TVDB_PLUGIN_GUID" ]; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
die "artifact guid is $guid, expected the official $TVDB_PLUGIN_GUID — refusing to install"
|
||||||
|
fi
|
||||||
|
if [ "$version" != "$TVDB_PLUGIN_VERSION" ]; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
die "artifact version is $version, pinned $TVDB_PLUGIN_VERSION — refusing to install"
|
||||||
|
fi
|
||||||
|
mkdir -p "$PLUGINS/$PLUGIN_DIR"
|
||||||
|
python3 -m zipfile -e "$tmp" "$PLUGINS/$PLUGIN_DIR/"
|
||||||
|
rm -f "$tmp"
|
||||||
|
[ -f "$PLUGINS/$PLUGIN_DIR/$TVDB_DLL" ] ||
|
||||||
|
die "$PLUGIN_DIR has no $TVDB_DLL — the artifact does not match this plugin"
|
||||||
|
printf '+ installed %s\n' "$PLUGIN_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_to_trash() {
|
||||||
|
local f="$1"
|
||||||
|
mkdir -p "$TRASH_DIR"
|
||||||
|
local dest="$TRASH_DIR/$(basename "$f").$(date +%H%M%S)"
|
||||||
|
cp -p "$f" "$dest"
|
||||||
|
printf '+ previous copy kept at %s\n' "$dest"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_write_keys() {
|
||||||
|
local now want
|
||||||
|
KEYS_CHANGED=0
|
||||||
|
# TMDb: the running container owns this file and every setting in it. Only
|
||||||
|
# the key element is touched, so country, language and adult settings
|
||||||
|
# survive the edit.
|
||||||
|
[ -f "$TMDB_XML" ] ||
|
||||||
|
die "$TMDB_XML is missing — save the TMDb plugin settings once in the UI, then re-run"
|
||||||
|
now="$(xml_value "$TMDB_XML" TmdbApiKey)"
|
||||||
|
keys_len TMDB_API_KEY >/dev/null
|
||||||
|
want="$(keys_value TMDB_API_KEY)"
|
||||||
|
if [ "$now" = "$want" ]; then
|
||||||
|
printf '= TmdbApiKey already set (%s chars)\n' "${#now}"
|
||||||
|
else
|
||||||
|
backup_to_trash "$TMDB_XML"
|
||||||
|
xml_secret "$TMDB_XML" TmdbApiKey TMDB_API_KEY
|
||||||
|
printf '+ set TmdbApiKey (%s chars)\n' "${#want}"
|
||||||
|
KEYS_CHANGED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TVDB: never overwrite a config the operator saved through the UI.
|
||||||
|
if [ -f "$TVDB_XML" ]; then
|
||||||
|
now="$(xml_value "$TVDB_XML" ApiKey)"
|
||||||
|
if [ -n "$now" ]; then
|
||||||
|
printf '= Jellyfin.Plugin.Tvdb.xml already has an ApiKey (kept)\n'
|
||||||
|
else
|
||||||
|
keys_len TVDB_API_KEY >/dev/null
|
||||||
|
backup_to_trash "$TVDB_XML"
|
||||||
|
xml_secret "$TVDB_XML" ApiKey TVDB_API_KEY
|
||||||
|
if [ -n "$(keys_value TVDB_PIN)" ] && [ -z "$(xml_value "$TVDB_XML" Pin)" ]; then
|
||||||
|
xml_secret "$TVDB_XML" Pin TVDB_PIN
|
||||||
|
fi
|
||||||
|
printf '+ filled the ApiKey in Jellyfin.Plugin.Tvdb.xml\n'
|
||||||
|
KEYS_CHANGED=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
keys_len TVDB_API_KEY >/dev/null
|
||||||
|
cat >"$TVDB_XML" <<XML
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
|
<ApiKey />
|
||||||
|
</Configuration>
|
||||||
|
XML
|
||||||
|
xml_secret "$TVDB_XML" ApiKey TVDB_API_KEY
|
||||||
|
if [ -n "$(keys_value TVDB_PIN)" ]; then
|
||||||
|
xml_secret "$TVDB_XML" Pin TVDB_PIN
|
||||||
|
fi
|
||||||
|
printf '+ created Jellyfin.Plugin.Tvdb.xml with ApiKey\n'
|
||||||
|
KEYS_CHANGED=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_plan() {
|
||||||
|
check_root
|
||||||
|
check_keys_report
|
||||||
|
printf 'root %s\n' "$JF_ROOT"
|
||||||
|
plugin_state
|
||||||
|
tmdb_state
|
||||||
|
tvdb_state
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_install() {
|
||||||
|
check_root
|
||||||
|
check_keys_report
|
||||||
|
KEYS_CHANGED=0
|
||||||
|
if [ ! -d "$PLUGINS/$PLUGIN_DIR" ]; then
|
||||||
|
write_plugin
|
||||||
|
KEYS_CHANGED=1
|
||||||
|
else
|
||||||
|
printf '= %s already installed\n' "$PLUGIN_DIR"
|
||||||
|
fi
|
||||||
|
cmd_write_keys
|
||||||
|
if [ "$KEYS_CHANGED" -eq 1 ]; then
|
||||||
|
# Nothing is restarted here on purpose: this host may be playing media
|
||||||
|
# right now, and both the plugin and its keys are read at start-up.
|
||||||
|
printf 'next: scripts/deploy.sh dankserver --service jellyfin (the plugin and keys load at start-up)\n'
|
||||||
|
else
|
||||||
|
printf '= no change\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_verify() {
|
||||||
|
check_root
|
||||||
|
local bad=0
|
||||||
|
if [ ! -d "$PLUGINS/$PLUGIN_DIR" ]; then
|
||||||
|
printf 'MISSING plugin directory %s (thetvdb not installed)\n' "$PLUGIN_DIR" >&2
|
||||||
|
bad=1
|
||||||
|
else
|
||||||
|
local dlls abi
|
||||||
|
dlls="$(find "$PLUGINS/$PLUGIN_DIR" -maxdepth 1 -name '*.dll' | wc -l)"
|
||||||
|
[ "$dlls" -ge 1 ] || {
|
||||||
|
printf 'MISSING %s inside %s\n' "$TVDB_DLL" "$PLUGIN_DIR" >&2
|
||||||
|
bad=1
|
||||||
|
}
|
||||||
|
abi="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("targetAbi","?"))' \
|
||||||
|
"$PLUGINS/$PLUGIN_DIR/meta.json" 2>/dev/null || echo '?')"
|
||||||
|
printf 'plugin %s: %s dll(s), targetAbi %s\n' "$PLUGIN_DIR" "$dlls" "$abi"
|
||||||
|
fi
|
||||||
|
local pair elem n
|
||||||
|
for pair in "$TMDB_XML|TmdbApiKey" "$TVDB_XML|ApiKey"; do
|
||||||
|
elem="${pair##*|}"
|
||||||
|
if [ ! -f "${pair%%|*}" ]; then
|
||||||
|
printf 'MISSING %s\n' "${pair%%|*}" >&2
|
||||||
|
bad=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! python3 -c 'import sys, xml.dom.minidom; xml.dom.minidom.parse(sys.argv[1])' "${pair%%|*}"; then
|
||||||
|
printf 'UNPARSEABLE %s\n' "${pair%%|*}" >&2
|
||||||
|
bad=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
n="$(xml_value "${pair%%|*}" "$elem")"
|
||||||
|
if [ -z "$n" ]; then
|
||||||
|
printf 'MISSING key: %s has no %s value\n' "$(basename "${pair%%|*}")" "$elem" >&2
|
||||||
|
bad=1
|
||||||
|
else
|
||||||
|
printf '%s %s chars\n' "$elem" "${#n}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "$bad" -eq 0 ] || return 1
|
||||||
|
printf 'providers configured\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="${1:-}"
|
||||||
|
case "$cmd" in
|
||||||
|
plan) cmd_plan ;;
|
||||||
|
install) cmd_install ;;
|
||||||
|
verify) cmd_verify ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Executable
+297
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
# Seam: scripts/jellyfin-providers.sh — the only mechanism that puts a
|
||||||
|
# metadata-provider key into Jellyfin's plugin configuration on Dankserver.
|
||||||
|
#
|
||||||
|
# Output vocabulary the tests depend on:
|
||||||
|
# "+ <action>" something this run will change
|
||||||
|
# "= <fact>" something already true, so nothing changes
|
||||||
|
#
|
||||||
|
# The contracts, in the order the seam tests them:
|
||||||
|
# 1. plan reports every action and never prints a key;
|
||||||
|
# 2. a secret file that is not mode 600 is refused by name and nothing is
|
||||||
|
# written — a key readable by other users is an error, not a warning;
|
||||||
|
# 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;
|
||||||
|
# 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
|
||||||
|
# overwrite without a recoverable copy);
|
||||||
|
# 8. verify answers whether the endpoint is configured, key LENGTH only;
|
||||||
|
# 9. no plugin configuration XML is tracked in git, the deployer places the
|
||||||
|
# keys BEFORE it restarts Jellyfin, and the planner routes a change to this
|
||||||
|
# installer to jellyfin.
|
||||||
|
|
||||||
|
load helpers
|
||||||
|
|
||||||
|
PROV="$REPO_ROOT/scripts/jellyfin-providers.sh"
|
||||||
|
TMDB_KEY="tmdbk3a9Zq7Rtestkey"
|
||||||
|
TVDB_KEY="tvdbk8b2Wt4Ptestkey"
|
||||||
|
GUID_OFFICIAL="a677c0da-fac5-4cde-941a-7134223f14c8"
|
||||||
|
|
||||||
|
# make_plugin <file> <guid> <version> — a zip shaped like the official TheTVDB
|
||||||
|
# artifact (meta.json + its two DLLs), so extraction is exercised for real.
|
||||||
|
make_plugin() {
|
||||||
|
python3 - "$1" "$2" "$3" <<'PYEOF'
|
||||||
|
import json, sys, zipfile
|
||||||
|
path, guid, version = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
|
meta = {
|
||||||
|
"category": "MoviesAndShows",
|
||||||
|
"guid": guid,
|
||||||
|
"name": "TheTVDB",
|
||||||
|
"owner": "jellyfin",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"version": version,
|
||||||
|
}
|
||||||
|
with zipfile.ZipFile(path, "w") as z:
|
||||||
|
z.writestr("meta.json", json.dumps(meta))
|
||||||
|
z.writestr("Jellyfin.Plugin.Tvdb.dll", "dll-bytes")
|
||||||
|
z.writestr("Tvdb.Sdk.dll", "dll-bytes")
|
||||||
|
PYEOF
|
||||||
|
sha256sum "$1" | cut -d' ' -f1
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
JF="$BATS_TEST_TMPDIR/jf"
|
||||||
|
TRASH="$BATS_TEST_TMPDIR/trash"
|
||||||
|
SEC="$BATS_TEST_TMPDIR/secrets"
|
||||||
|
mkdir -p "$JF/plugins/configurations" "$TRASH" "$SEC"
|
||||||
|
cat >"$JF/plugins/configurations/Jellyfin.Plugin.Tmdb.xml" <<XML
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
|
<MetadataCountryCode>US</MetadataCountryCode>
|
||||||
|
<PreferOriginalLanguage />
|
||||||
|
<Language>en</Language>
|
||||||
|
<EnableCinematicForTrailers>false</EnableCinematicForTrailers>
|
||||||
|
<IncludeAdult>false</IncludeAdult>
|
||||||
|
<MaxCastMembers>25</MaxCastMembers>
|
||||||
|
<AddGenresFromKeywords>true</AddGenresFromKeywords>
|
||||||
|
<TmdbApiKey />
|
||||||
|
</Configuration>
|
||||||
|
XML
|
||||||
|
KEYS="$SEC/provider-keys"
|
||||||
|
printf 'TMDB_API_KEY=%s\nTVDB_API_KEY=%s\n' "$TMDB_KEY" "$TVDB_KEY" >"$KEYS"
|
||||||
|
chmod 600 "$KEYS"
|
||||||
|
ZIP="$BATS_TEST_TMPDIR/thetvdb.zip"
|
||||||
|
PIN_SHA="$(make_plugin "$ZIP" "$GUID_OFFICIAL" 24.0.0.0)"
|
||||||
|
PROV_ENV=(
|
||||||
|
JF_ROOT="$JF"
|
||||||
|
PROV_KEYS="$KEYS"
|
||||||
|
TRASH_DIR="$TRASH"
|
||||||
|
TVDB_PLUGIN_URL="file://$ZIP"
|
||||||
|
TVDB_PLUGIN_SHA256="$PIN_SHA"
|
||||||
|
TVDB_PLUGIN_GUID="$GUID_OFFICIAL"
|
||||||
|
TVDB_PLUGIN_VERSION=24.0.0.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
prov() { env "${PROV_ENV[@]}" bash "$PROV" "$@"; }
|
||||||
|
|
||||||
|
tree_fingerprint() { find "$JF" -type f | sort | xargs -r sha256sum | sha256sum; }
|
||||||
|
|
||||||
|
@test "plan names the plugin and both config files, and prints no key" {
|
||||||
|
run prov plan
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -q '^+ install thetvdb_24\.0\.0\.0'
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -q '^+ set TmdbApiKey'
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -q '^+ create Jellyfin\.Plugin\.Tvdb\.xml'
|
||||||
|
# The whole point of running this over a shoulder: a key never appears.
|
||||||
|
! printf '%s\n' "${lines[@]}" | grep -qF "$TMDB_KEY"
|
||||||
|
! printf '%s\n' "${lines[@]}" | grep -qF "$TVDB_KEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "plan writes nothing" {
|
||||||
|
before="$(tree_fingerprint)"
|
||||||
|
run prov plan
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$(tree_fingerprint)" = "$before" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "an already-configured endpoint reports no pending action" {
|
||||||
|
prov install >/dev/null
|
||||||
|
run prov plan
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
! printf '%s\n' "${lines[@]}" | grep -q '^+ '
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -q '= thetvdb_24\.0\.0\.0 already installed'
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -q '= TmdbApiKey already set'
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -q '= Jellyfin\.Plugin\.Tvdb\.xml already has an ApiKey'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "a keys file that is not mode 600 is refused and nothing is written" {
|
||||||
|
chmod 644 "$KEYS"
|
||||||
|
before="$(tree_fingerprint)"
|
||||||
|
run prov install
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qF '600'
|
||||||
|
[ "$(tree_fingerprint)" = "$before" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "a missing TVDB key is refused by name before anything is fetched" {
|
||||||
|
printf 'TMDB_API_KEY=%s\n' "$TMDB_KEY" >"$KEYS"
|
||||||
|
chmod 600 "$KEYS"
|
||||||
|
run prov install
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qF 'TVDB_API_KEY'
|
||||||
|
[ ! -d "$JF/plugins/thetvdb_24.0.0.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "a root with no plugin directory is refused instead of fabricated" {
|
||||||
|
# Creating plugins/ inside a wrong path would install a plugin Jellyfin
|
||||||
|
# never loads, and would hide the operator's typo.
|
||||||
|
run env "${PROV_ENV[@]}" JF_ROOT="$BATS_TEST_TMPDIR/nope" bash "$PROV" install
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[ ! -d "$BATS_TEST_TMPDIR/nope/plugins" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "an artifact whose sha256 is not the pinned one is refused" {
|
||||||
|
run env "${PROV_ENV[@]}" TVDB_PLUGIN_SHA256="$(printf 'f%.0s' {1..64})" \
|
||||||
|
bash "$PROV" install
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qiF 'sha256'
|
||||||
|
[ ! -d "$JF/plugins/thetvdb_24.0.0.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "an artifact that is not the official TVDB plugin is refused" {
|
||||||
|
# A matching sha proves only that the bytes are the bytes we fetched.
|
||||||
|
EVIL="$BATS_TEST_TMPDIR/evil.zip"
|
||||||
|
EVIL_SHA="$(make_plugin "$EVIL" "deadbeef-0000-0000-0000-000000000000" 24.0.0.0)"
|
||||||
|
run env "${PROV_ENV[@]}" TVDB_PLUGIN_URL="file://$EVIL" TVDB_PLUGIN_SHA256="$EVIL_SHA" \
|
||||||
|
bash "$PROV" install
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qiF 'guid'
|
||||||
|
[ ! -d "$JF/plugins/thetvdb_24.0.0.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "an artifact that is not the pinned version is refused" {
|
||||||
|
OLD="$BATS_TEST_TMPDIR/old.zip"
|
||||||
|
OLD_SHA="$(make_plugin "$OLD" "$GUID_OFFICIAL" 22.0.0.0)"
|
||||||
|
run env "${PROV_ENV[@]}" TVDB_PLUGIN_URL="file://$OLD" TVDB_PLUGIN_SHA256="$OLD_SHA" \
|
||||||
|
bash "$PROV" install
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qiF 'version'
|
||||||
|
[ ! -d "$JF/plugins/thetvdb_24.0.0.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "a different plugin version already in plugins/ is refused, not doubled" {
|
||||||
|
# Two thetvdb_* directories means Jellyfin loads one plugin twice; the
|
||||||
|
# operator removes the old one (into the trash) deliberately, not by an
|
||||||
|
# installer quietly adding a second.
|
||||||
|
prov install >/dev/null
|
||||||
|
OLD="$BATS_TEST_TMPDIR/old.zip"
|
||||||
|
OLD_SHA="$(make_plugin "$OLD" "$GUID_OFFICIAL" 22.0.0.0)"
|
||||||
|
run env "${PROV_ENV[@]}" TVDB_PLUGIN_URL="file://$OLD" TVDB_PLUGIN_SHA256="$OLD_SHA" \
|
||||||
|
TVDB_PLUGIN_VERSION=22.0.0.0 bash "$PROV" install
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qF 'thetvdb_24.0.0.0'
|
||||||
|
[ ! -d "$JF/plugins/thetvdb_22.0.0.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "install places the plugin, keys TMDb, and preserves every other setting" {
|
||||||
|
run prov install
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
! printf '%s\n' "${lines[@]}" | grep -qF "$TMDB_KEY"
|
||||||
|
! printf '%s\n' "${lines[@]}" | grep -qF "$TVDB_KEY"
|
||||||
|
[ -f "$JF/plugins/thetvdb_24.0.0.0/meta.json" ]
|
||||||
|
[ -f "$JF/plugins/thetvdb_24.0.0.0/Jellyfin.Plugin.Tvdb.dll" ]
|
||||||
|
local cfg="$JF/plugins/configurations/Jellyfin.Plugin.Tmdb.xml"
|
||||||
|
# Jellyfin owns this file: the settings it wrote must survive the edit.
|
||||||
|
for setting in MetadataCountryCode Language IncludeAdult MaxCastMembers AddGenresFromKeywords; do
|
||||||
|
grep -qF "<$setting>" "$cfg" || {
|
||||||
|
echo "lost $setting"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
grep -qF '<IncludeAdult>false</IncludeAdult>' "$cfg"
|
||||||
|
grep -qF "<TmdbApiKey>$TMDB_KEY</TmdbApiKey>" "$cfg"
|
||||||
|
grep -qF "<ApiKey>$TVDB_KEY</ApiKey>" "$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "a TVDB config the operator saved through the UI is kept" {
|
||||||
|
cat >"$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml" <<XML
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
|
<ApiKey>ui-saved-key</ApiKey>
|
||||||
|
<EnableSeasonPathMatching>true</EnableSeasonPathMatching>
|
||||||
|
</Configuration>
|
||||||
|
XML
|
||||||
|
run prov install
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qiF 'kept'
|
||||||
|
local cfg="$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml"
|
||||||
|
grep -qF '<ApiKey>ui-saved-key</ApiKey>' "$cfg"
|
||||||
|
grep -qF '<EnableSeasonPathMatching>true</EnableSeasonPathMatching>' "$cfg"
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.
|
||||||
|
printf 'TMDB_API_KEY=%s\nTVDB_API_KEY=%s\nTVDB_PIN=123456\n' "$TMDB_KEY" "$TVDB_KEY" >"$KEYS"
|
||||||
|
chmod 600 "$KEYS"
|
||||||
|
prov install >/dev/null
|
||||||
|
grep -qF '<Pin>123456</Pin>' "$JF/plugins/configurations/Jellyfin.Plugin.Tvdb.xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "install is idempotent: a second run changes no byte" {
|
||||||
|
prov install >/dev/null
|
||||||
|
before="$(tree_fingerprint)"
|
||||||
|
run prov install
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$(tree_fingerprint)" = "$before" ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qiF 'no change'
|
||||||
|
# One plugin directory only: two versions in plugins/ means two loads.
|
||||||
|
[ "$(find "$JF/plugins" -maxdepth 1 -type d -name 'thetvdb_*' | wc -l)" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "what install replaces is copied to the trash first" {
|
||||||
|
prov install >/dev/null
|
||||||
|
# The TMDb config existed before this tool touched it, so the pre-edit copy
|
||||||
|
# must stay recoverable.
|
||||||
|
[ -n "$(find "$TRASH" -name 'Jellyfin.Plugin.Tmdb.xml*' -print -quit)" ]
|
||||||
|
grep -qF '<TmdbApiKey />' "$(find "$TRASH" -name 'Jellyfin.Plugin.Tmdb.xml*' | head -1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "verify refuses an unconfigured endpoint and names what is missing" {
|
||||||
|
run prov verify
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qiF 'thetvdb'
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qiF 'TmdbApiKey'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "verify passes once the plugin and both keys are in place" {
|
||||||
|
prov install >/dev/null
|
||||||
|
run prov verify
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
! printf '%s\n' "${lines[@]}" | grep -qF "$TMDB_KEY"
|
||||||
|
! printf '%s\n' "${lines[@]}" | grep -qF "$TVDB_KEY"
|
||||||
|
# Length, not bytes: the operator sees the key loaded without reading it.
|
||||||
|
printf '%s\n' "${lines[@]}" | grep -qE 'TmdbApiKey .*[0-9]+ chars'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "no plugin configuration XML is tracked in this repo" {
|
||||||
|
# These files hold provider keys and are owned by the running container;
|
||||||
|
# tracking them would put credentials in git and guarantee drift.
|
||||||
|
run git -C "$REPO_ROOT" ls-files
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
! printf '%s\n' "${lines[@]}" | grep -qF 'plugins/configurations'
|
||||||
|
! printf '%s\n' "${lines[@]}" | grep -qiF 'Plugin.Tvdb.xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the planner routes this installer to jellyfin" {
|
||||||
|
run python3 "$REPO_ROOT/scripts/deploy-plan.py" --target dankserver \
|
||||||
|
--paths scripts/jellyfin-providers.sh
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "jellyfin" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the deployer places the keys before it restarts jellyfin" {
|
||||||
|
# The plugin and both keys are read at start-up, so a restart that runs
|
||||||
|
# ahead of the placement boots the old, keyless state.
|
||||||
|
body="$(sed -n '/^deploy_jellyfin()/,/^\}/p' "$REPO_ROOT/scripts/deploy-remote.sh")"
|
||||||
|
[ -n "$body" ]
|
||||||
|
up="$(printf '%s\n' "$body" | grep -n 'ensure_provider_keys' | head -1 | cut -d: -f1)"
|
||||||
|
restart="$(printf '%s\n' "$body" | grep -n 'systemctl --user restart jellyfin.service' | head -1 | cut -d: -f1)"
|
||||||
|
[ "${up:-999}" -lt "${restart:-0}" ]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user