File the owner's containers on the mirror during onboard, with the icon resolved where the Emby key is

This commit is contained in:
Gmer4Lfe
2026-08-17 05:33:52 -04:00
parent 6dbb076a1e
commit fe58fb7826
3 changed files with 116 additions and 4 deletions
+53
View File
@@ -42,6 +42,8 @@
# Step 10: Conf push — push master.conf + setup state to all listed hosts
# Step 11: Service discovery — conf_populate.sh on the mirror, last, once the stacks it
# would discover are actually deployed there
# Step 12: Container grouping — file our containers on the mirror under "<OwnerShort>-Fallback",
# icon resolved here and passed over: the mirror has no Emby key
#
# ==============================================================================================
# DESIGN PRINCIPLES
@@ -1047,6 +1049,56 @@ else
unset _mirror_sd _pop_script
fi
# ── Step 12: Group our containers on the mirror ───────────────────────────────────────────────
# The mirror now runs a dozen containers that are ours, scattered among its own. This files them
# under one folder named after us — "<OwnerShort>-Fallback" — matching the convention the owner
# already keeps for the mirror's containers.
#
# The icon is resolved HERE and passed over, not looked up there. It comes from the closest Emby
# user to our own name, and the mirror has neither our Emby key nor necessarily an Emby at all —
# so a lookup on that side would find nothing and the folder would come up blank.
#
# Not fatal in any direction: folder.view3 absent on the mirror is a clean skip, and a folder
# without a picture is still a folder.
echo ""
echo "━━━ $ICON_GEAR Step 12 — Container Grouping ($MIRROR) ━━━"
FOLDER_OK=false
if [[ -z "${MIRROR_IP:-}" ]]; then
warn "$MIRROR has no resolved IP — skipping container grouping"
FOLDER_OK=skipped
else
mapfile -t _deployed < <(deployed_stack_container_names)
_deployed_csv=$(IFS=,; echo "${_deployed[*]}")
if [[ -z "$_deployed_csv" ]]; then
log "No stack templates resolved to container names — nothing to group"
FOLDER_OK=skipped
elif [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would create ${MY_ID}-named fallback folder on $MIRROR with: $_deployed_csv"
FOLDER_OK=true
else
_ff_local="$SCRIPTS_ROOT/Plugin/$PLATFORM/Tools/fallback_folder.php"
_icon=$(php "$_ff_local" --host="$MY_ID" --icon-only 2>/dev/null || true)
[[ -z "$_icon" ]] && log "No icon resolved for $MY_ID — folder will be created without one"
_mirror_sd=$(resolve_remote_scripts_dir "$MIRROR_IP" "$MIRROR_SSH_KEY" "no")
_ff_remote="${_mirror_sd}/Plugin/${PLATFORM}/Tools/fallback_folder.php"
if timeout 60 ssh -i "$MIRROR_SSH_KEY" \
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
root@"$MIRROR_IP" \
"[ -f '$_ff_remote' ] || { echo missing; exit 127; }
php '$_ff_remote' --host=$(printf '%q' "$MY_ID") \
--containers=$(printf '%q' "$_deployed_csv") \
--icon=$(printf '%q' "$_icon")" 2>/dev/null; then
FOLDER_OK=true
else
warn "Could not group containers on $MIRROR — run $_ff_remote there by hand"
fi
unset _ff_local _ff_remote _mirror_sd _icon
fi
unset _deployed _deployed_csv
fi
# ── Write Phase 2 completion state ────────────────────────────────────────────────────────────
[[ "$ONBOARD_OK" == true && "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 2
@@ -1080,6 +1132,7 @@ echo " Step 9d — Media seed: $( [[ "$SKIP_MEDIA_SEED" == true ]] && ech
echo " Step 9e — Webhook listener: $(_skip "$SKIP_WEBHOOK_LISTENER" "$WEBHOOK_LISTENER_OK")"
echo " Step 10 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"
echo " Step 11 — Discovery: $( [[ "$POPULATE_OK" == skipped ]] && echo "skipped (unreachable)" || _ok "$POPULATE_OK" )"
echo " Step 12 — Grouping: $( [[ "$FOLDER_OK" == skipped ]] && echo "skipped" || _ok "$FOLDER_OK" )"
echo ""
if [[ "$ONBOARD_OK" == true ]]; then
+25
View File
@@ -454,6 +454,31 @@ deploy_xml_stack() {
#
# Usage: ensure_stack_networks_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
# ==============================================================================================
# ==============================================================================================
# ── Container names this onboard deploys, read from the templates it deploys them from ────────
#
# Echoes one name per line. The <Name> element is the same value deploy_xml_stack() passes to
# `docker create --name`, so this is the deployed set by construction rather than by asking the
# mirror what it ended up with — which would also pick up whatever the mirror already ran.
#
# Usage: mapfile -t names < <(deployed_stack_container_names)
# ==============================================================================================
deployed_stack_container_names() {
local -a xml_names=()
[[ ${#PARTNERSHIP_AUTH_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")
[[ ${#PARTNERSHIP_ARR_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")
[[ ${#PARTNERSHIP_SERVICES_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_SERVICES_STACK[@]}")
local xml_name xml_file cname
for xml_name in "${xml_names[@]}"; do
[[ -z "$xml_name" ]] && continue
xml_file="${TEMPLATES_DIR}/${xml_name}"
[[ -f "$xml_file" ]] || continue
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
[[ -n "$cname" ]] && echo "$cname"
done
}
ensure_stack_networks_on_remote() {
local remote_ip="$1" ssh_key="$2"
local -a xml_names=() nets=()
+38 -4
View File
@@ -40,7 +40,8 @@
// Icon resolution failing never blocks the folder: no image is a cosmetic loss, no folder is not.
//
// REQUEST
// fallback_folder.php --host=HOST2 [--containers=A,B,C] [--dry-run]
// fallback_folder.php --host=HOST2 [--containers=A,B,C] [--icon=URL] [--dry-run]
// fallback_folder.php --host=HOST2 --icon-only resolve and print the icon URL, write nothing
//
// CONFIGURATION
// HOST1/HOST2… master.conf — the hostname the folder is named after
@@ -57,9 +58,10 @@ require_once $pluginDir . '/include/config.php';
define('FV3_JSON', '/boot/config/plugins/folder.view3/docker.json');
// ── Args ──────────────────────────────────────────────────────────────────────
$opts = ['host' => '', 'containers' => '', 'dry-run' => false];
$opts = ['host' => '', 'containers' => '', 'icon' => '', 'dry-run' => false, 'icon-only' => false];
foreach (array_slice($argv, 1) as $a) {
if ($a === '--dry-run') { $opts['dry-run'] = true; continue; }
if ($a === '--dry-run') { $opts['dry-run'] = true; continue; }
if ($a === '--icon-only'){ $opts['icon-only'] = true; continue; }
if (preg_match('/^--([a-z-]+)=(.*)$/', $a, $m)) $opts[$m[1]] = $m[2];
}
$hostId = strtoupper(trim($opts['host']));
@@ -72,6 +74,7 @@ $vars = vv_conf_vars();
$hostname = trim($vars[$hostId] ?? '');
if ($hostname === '') { fwrite(STDERR, "$hostId is not set in master.conf\n"); exit(1); }
// derive_short_name() in common.sh: lowercase, strip a leading "unraid-", capitalise.
$short = preg_replace('/^unraid-/i', '', $hostname);
$short = ucfirst($short);
@@ -124,9 +127,40 @@ function vv_closest_emby_icon(string $short, array $vars, string $meId): array {
}
$meId = strtoupper(vv_detect_host());
[$icon, $iconNote] = vv_closest_emby_icon($short, $vars, $meId);
// --icon= skips the lookup entirely. The mirror needs this: it is being given a folder named
// after the OWNER, and the avatar lives in the owner's Emby — which the mirror has no key for and
// may not run at all. So the owner resolves the URL with --icon-only and hands it over, rather
// than the mirror guessing from a userbase it cannot see.
if (trim($opts['icon']) !== '') {
$icon = trim($opts['icon']);
$iconNote = 'supplied by caller';
} else {
[$icon, $iconNote] = vv_closest_emby_icon($short, $vars, $meId);
}
// --icon-only: resolve and print, touch nothing. Exits non-zero when there is no icon, so a
// caller can tell "no image" from "empty string because something broke".
if ($opts['icon-only']) {
if ($icon === '') { fwrite(STDERR, "no icon: $iconNote\n"); exit(1); }
echo $icon . "\n";
exit(0);
}
// ── Upsert the folder ─────────────────────────────────────────────────────────
// A host does not keep a fallback folder for itself — the folder means "containers I run on
// SOMEONE ELSE's behalf", so naming it after this machine is always wrong. Checked here rather
// than at argument parsing, because --icon-only legitimately asks for THIS host's own avatar:
// the owner resolves its own picture to hand to the mirror, which is the whole point of that mode.
//
// Refused rather than created, because the failure is otherwise silent — an empty folder named
// after yourself looks plausible enough to survive a glance. One appeared on HOST1 exactly this
// way, when an older copy of this script ignored an unrecognised flag and ran the upsert anyway.
if (strcasecmp($hostId, strtoupper(vv_detect_host())) === 0) {
fwrite(STDERR, "$hostId is this host — a fallback folder is named after the PARTNER, not self\n");
exit(2);
}
if (!file_exists(FV3_JSON)) {
echo "folder.view3 is not installed — nothing to do\n";
exit(0);