diff --git a/Plugin/unraid/Tools/remote_arr_cache_writer.sh b/Plugin/unraid/Tools/remote_arr_cache_writer.sh index 3598b60..12c9f30 100755 --- a/Plugin/unraid/Tools/remote_arr_cache_writer.sh +++ b/Plugin/unraid/Tools/remote_arr_cache_writer.sh @@ -152,9 +152,11 @@ for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do "php -r \" require_once '/usr/local/emhttp/plugins/varaverk/include/arrs.php'; require_once '/usr/local/emhttp/plugins/varaverk/include/unraid_api.php'; + require_once '/usr/local/emhttp/plugins/varaverk/include/media.php'; echo json_encode([ 'arrs' => vv_arrs_local_node(), 'monitor' => vv_local_host_stats(), + 'media' => function_exists('vv_media_server_stats') ? vv_media_server_stats() : [], ]); \"" 2>/dev/null) @@ -188,6 +190,19 @@ for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do file_put_contents('$MONITOR_CACHE', json_encode(\$d['monitor'])); " 2>/dev/null + # Save media server cache. Collected here rather than queried directly because a partner's + # Emby URL is http://localhost:8096 — true on that host and meaningless from this one. The + # same reason the arrs are collected this way: each host answers about itself, and its API + # keys never leave it. + # + # Absent on a partner running an older build, which is why it is written only when present: + # an empty file would read as "no media servers there" rather than "not collected yet". + MEDIA_CACHE="$VV_CACHE_DIR/media_remote_${host_id}.json" + echo "$RESULT" | php -r " + \$d = json_decode(file_get_contents('php://stdin'), true); + if (!empty(\$d['media'])) file_put_contents('$MEDIA_CACHE', json_encode(\$d['media'])); + " 2>/dev/null + CACHED_TYPES=$(echo "$RESULT" | php -r " \$d = json_decode(file_get_contents('php://stdin'), true); echo implode(', ', array_column(\$d['arrs']['arrs'] ?? [], 'type')); diff --git a/Plugin/unraid/include/arrs.php b/Plugin/unraid/include/arrs.php index a11e12e..9598df3 100644 --- a/Plugin/unraid/include/arrs.php +++ b/Plugin/unraid/include/arrs.php @@ -486,7 +486,7 @@ function vv_arrs_all(): array { // their own. Cheap — three run records and three log tails — and it rides this payload // rather than a fourth endpoint because it is shown on this page and cached with it. 'media_jobs' => function_exists('vv_media_jobs') ? vv_media_jobs() : [], - 'media_servers' => function_exists('vv_media_server_stats') ? vv_media_server_stats() : [], + 'media_servers' => function_exists('vv_media_servers_mesh') ? vv_media_servers_mesh() : [], 'host' => $currentHost, 'settings' => [ 'arr_sync_enabled' => ($vars['ARR_SYNC_ENABLED'] ?? 'true') !== 'false', diff --git a/Plugin/unraid/include/media.php b/Plugin/unraid/include/media.php index dea24a4..3475e92 100644 --- a/Plugin/unraid/include/media.php +++ b/Plugin/unraid/include/media.php @@ -367,6 +367,46 @@ function vv_media_server_stats(): array { return $out; } +// ── The mesh view ───────────────────────────────────────────────────────────────────────────── +// Local live, partners from cache — the same split the arrs use, for the same reason. A partner's +// Emby URL is http://localhost:8096, which is true on that host and meaningless here, so nobody +// queries a partner's media server directly. Each host answers about itself over SSH in +// remote_arr_cache_writer.sh and its API keys never leave it. +// +// A partner with no cache is reported as not collected rather than as having no media servers. +// Those are different states and only one of them is the partner's fault — the arrs page already +// makes that distinction and this matches it rather than inventing a second vocabulary. +function vv_media_servers_mesh(): array { + $me = vv_detect_host(); + $names = function_exists('vv_known_hosts') ? vv_known_hosts() : [$me => $me]; + $out = []; + + foreach (array_keys($names) as $h) { + if ($h === $me) { + $out[] = [ + 'host' => $h, + 'name' => $names[$h] ?? $h, + 'local' => true, + 'servers' => vv_media_server_stats(), + ]; + continue; + } + $f = VV_CACHE_DIR . '/media_remote_' . $h . '.json'; + $raw = @file_get_contents($f); + $d = $raw !== false ? json_decode($raw, true) : null; + $out[] = [ + 'host' => $h, + 'name' => $names[$h] ?? $h, + 'local' => false, + 'cached' => is_array($d), + 'cache_miss' => !is_array($d), + 'cache_age' => is_array($d) ? max(0, time() - (int)@filemtime($f)) : null, + 'servers' => is_array($d) ? $d : [], + ]; + } + return $out; +} + // ── The media jobs that had nowhere to be seen ──────────────────────────────────────────────── // Three scripts that operate on the media files every day and appeared in no tab: play state sync // every thirty minutes, permissions and the cleaner nightly. The only way to know whether any of diff --git a/Plugin/unraid/pages/arrs.php b/Plugin/unraid/pages/arrs.php index d1a889e..6fdd29d 100644 --- a/Plugin/unraid/pages/arrs.php +++ b/Plugin/unraid/pages/arrs.php @@ -399,9 +399,41 @@ function _dur(s) { // long it has been up. A server answering happily at 145% CPU is a different situation from one // answering happily at 8%, and neither half says so alone. let _vvMsFilter = 'all'; +// Local by default. The mesh view is the deliberate act of asking about the whole estate; opening +// the tab to check your own server should not first make you read past a partner's. +let _vvMsScope = 'local'; -function _mediaServersSection(servers) { - if (!servers || !servers.length) return ''; +function _mediaServersSection(nodes) { + if (!nodes || !nodes.length) return ''; + + const me = nodes.filter(n => n.local); + const others = nodes.filter(n => !n.local); + const inScope = _vvMsScope === 'mesh' ? nodes : me; + + // Flattened with their host attached, so a card can say which machine it is on once more than + // one is shown. Without that a mesh view is two Embys and no way to tell them apart. + const servers = []; + for (const n of inScope) for (const s of (n.servers || [])) { + servers.push({ ...s, _host: n.host, _hostName: n.name, _local: !!n.local, _age: n.cache_age }); + } + + // Only offered when there is a partner to combine with. On a single-host install the control + // would be two words for the same view. + const scopeTabs = others.length + ? `