Let the media server card show the whole mesh, not just this host
A partner's Emby URL is http://localhost:8096 — true there, meaningless here — so nobody queries a partner's media server directly. Each host reports its own over the SSH call remote_arr_cache_writer.sh already makes, which keeps its API keys on it and costs no extra connection. Local is the default: opening the tab to check your own server should not first make you read past a partner's. A partner with no cache is stated as not collected rather than omitted, because an empty mesh view and a partner that is down look identical otherwise.
This commit is contained in:
@@ -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'));
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
? `<div style="display:flex;gap:4px;margin-left:auto;">` +
|
||||
[['local', 'This host'], ['mesh', 'Mesh']].map(([k, lbl]) =>
|
||||
`<button class="vv-arr-mstab${_vvMsScope === k ? ' on' : ''}" data-msscope="${vvEscAttr(k)}">${vvEscHtml(lbl)}</button>`
|
||||
).join('') + `</div>`
|
||||
: '';
|
||||
|
||||
// A partner that has never been collected is stated, not omitted — an empty mesh view and a
|
||||
// partner that is simply down look identical otherwise.
|
||||
const missing = _vvMsScope === 'mesh'
|
||||
? others.filter(n => n.cache_miss).map(n =>
|
||||
`<div style="grid-column:1/-1;font-size:10px;color:#3f3f3f;padding:2px 0;">
|
||||
${vvEscHtml(n.name)} — not collected yet; the partner reports its own media servers over SSH every 2 hours
|
||||
</div>`).join('')
|
||||
: '';
|
||||
|
||||
const types = [...new Set(servers.map(s => s.type))];
|
||||
// The filter only earns its space when there is something to filter. One server means the
|
||||
@@ -436,6 +468,9 @@ function _mediaServersSection(servers) {
|
||||
<span><span class="vv-arr-dot" style="background:${dot}"></span>
|
||||
<span class="vv-arr-ver">${vvEscHtml(up ? (s.version || 'online') : 'offline')}</span></span>
|
||||
</div>
|
||||
${_vvMsScope === 'mesh' ? `<div style="font-size:9px;color:#3a3a3a;margin:-6px 0 8px;">
|
||||
${vvEscHtml(s._hostName || s._host || '')}${s._local ? '' : ' · cached' + (s._age != null ? ' ' + _relTime(Math.floor(Date.now()/1000) - s._age) : '')}
|
||||
</div>` : ''}
|
||||
${up ? `
|
||||
<div class="vv-arr-stats">
|
||||
<div class="vv-arr-stat"><div class="vv-arr-stat-n${s.sessions ? '' : ' dim'}">${_n(s.sessions)}</div><div class="vv-arr-stat-l">Streams</div></div>
|
||||
@@ -455,9 +490,12 @@ function _mediaServersSection(servers) {
|
||||
}).join('');
|
||||
|
||||
return `<div style="grid-column:1/-1;">
|
||||
<div style="font-size:11px;font-weight:700;color:#555;text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;">Media servers</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||||
<span style="font-size:11px;font-weight:700;color:#555;text-transform:uppercase;letter-spacing:.07em;">Media servers</span>
|
||||
${scopeTabs}
|
||||
</div>
|
||||
${tabs}
|
||||
<div class="vv-arr-cards">${cards}</div>
|
||||
<div class="vv-arr-cards">${cards}${missing}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -467,7 +505,8 @@ function _mediaServersSection(servers) {
|
||||
document.addEventListener('click', ev => {
|
||||
const b = ev.target.closest('.vv-arr-mstab');
|
||||
if (!b) return;
|
||||
_vvMsFilter = b.dataset.mstab || 'all';
|
||||
if (b.dataset.msscope) _vvMsScope = b.dataset.msscope;
|
||||
else _vvMsFilter = b.dataset.mstab || 'all';
|
||||
if (_vvArrsLast) _render(_vvArrsLast);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user