The Movies library had indexed 2,571 films and had never asked a provider
for a single one: no TMDb id, no plot, no rating, no poster. Nothing was
broken in the provider chain — the plugin is core in 12.4 and active, its
key is set, image.tmdb.org answers the container — and nothing was broken
in the write path: the overlay view has been read-write since the 2026-10-31
cutover, and movie.nfo lands on the NVMe upper layer today.
The state was two settings in the per-library option file,
config/root/default/<Library>/options.xml (Jellyfin 12 renamed it from
library.xml; the database stores none of these options, so a census that
greps for the old name and then queries the DB concludes nothing is
configured and goes hunting elsewhere):
<EnableInternetProviders>false</EnableInternetProviders>
<TypeOptions><ImageFetchers /> (empty, beside a populated
<MetadataFetchers> and a fully
populated <ImageFetcherOrder>)
With the internet switch off every refresh answers 204, stores nothing and
logs nothing, so the item count reads like a healthy scan — this passed the
deploy health gate, the Playwright suite and three agent audits. Order is
not enablement: the fetcher ORDER stays populated while the ENABLED set sits
empty, and the populated metadata list beside it makes the library look well.
verify now audits every library's options.xml and fails with `SILENT INDEX
<Library>: <what>` on providers-off or on a type whose image fetchers are
empty while its metadata fetchers are named; an options.xml that never
carried the switch (BoxSets, written before the property existed) is the
vendor default and stays accepted.
Vault: the page claimed artwork is a sidecar like movie.nfo. It is not — no
image saver is bound for any library, so art goes to the internal store and
Plex never reads it. The claim is corrected, and the RemoteImages diagnostic
that separates a provider fault from a library fault is written down with
the 12.x API shape traps (Items needs UserId; MetadataRefreshMode has no
Full/ValidationFailure, ReplaceAllMetadata=true is what re-runs providers).
452 lines
17 KiB
Bash
Executable File
452 lines
17 KiB
Bash
Executable File
#!/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 in place, and that no library
|
|
# is a silent file index (providers off / image fetchers empty)
|
|
#
|
|
# 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
|
|
}
|
|
|
|
|
|
# A library whose options.xml carries EnableInternetProviders=false, or an empty
|
|
# ImageFetchers list beside a populated MetadataFetchers list, is a SILENT FILE
|
|
# INDEX: it indexes every file, answers 204 to every refresh, stores no provider
|
|
# id, plot, rating or artwork, and logs nothing — so the item count reads like a
|
|
# healthy scan. Jellyfin 12 keeps these options in root/default/<Library>/options.xml
|
|
# (it was library.xml) and stores NONE of them in the database, so this audit is
|
|
# the only place the state can be read without a browser session.
|
|
verify_library_options() {
|
|
local root="$JF_ROOT/root/default" f lib problems
|
|
if [ ! -d "$root" ]; then
|
|
printf '= no library options under %s (nothing audited)\n' "$root"
|
|
return 0
|
|
fi
|
|
local rc=0
|
|
for f in "$root"/*/options.xml; do
|
|
[ -f "$f" ] || continue
|
|
lib="$(basename "$(dirname "$f")")"
|
|
problems="$(python3 -s - "$f" <<'PY'
|
|
import sys, xml.dom.minidom as X
|
|
root = X.parse(sys.argv[1]).documentElement
|
|
def child(el, name):
|
|
for n in el.childNodes:
|
|
if n.nodeType == n.ELEMENT_NODE and n.tagName == name:
|
|
return n
|
|
def flag(el, name):
|
|
n = child(el, name)
|
|
return None if n is None else (n.firstChild.data.strip() if n.firstChild else "")
|
|
def names(el, name):
|
|
n = child(el, name)
|
|
return None if n is None else [s.firstChild.data.strip() for s in n.childNodes
|
|
if s.nodeType == s.ELEMENT_NODE and s.firstChild]
|
|
bad = []
|
|
ip = flag(root, "EnableInternetProviders")
|
|
if ip is not None and ip.lower() != "true":
|
|
bad.append("EnableInternetProviders=%s" % (ip or "empty"))
|
|
types = child(root, "TypeOptions")
|
|
if types is not None:
|
|
for t in types.childNodes:
|
|
if t.nodeType != t.ELEMENT_NODE or t.tagName != "TypeOptions":
|
|
continue
|
|
meta = names(t, "MetadataFetchers") or []
|
|
if meta and not (names(t, "ImageFetchers") or []):
|
|
bad.append("%s: ImageFetchers empty while MetadataFetchers names %d"
|
|
% (flag(t, "Type") or "?", len(meta)))
|
|
print("; ".join(bad))
|
|
PY
|
|
)"
|
|
if [ -n "$problems" ]; then
|
|
printf 'SILENT INDEX %s: %s\n' "$lib" "$problems" >&2
|
|
rc=1
|
|
else
|
|
printf '= %s: library options fetch from providers\n' "$lib"
|
|
fi
|
|
done
|
|
return "$rc"
|
|
}
|
|
|
|
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
|
|
verify_library_options || bad=1
|
|
[ "$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
|