Arrs: remote host cards via SSH cache, vv_arrs_local_node(), 2h refresh schedule
This commit is contained in:
@@ -2,6 +2,38 @@
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
// ── Manual remote refresh — runs remote_arr_cache_writer for one host ─────────
|
||||
$_action = trim($_GET['action'] ?? '');
|
||||
if ($_action === 'refresh_remote') {
|
||||
$host = strtolower(trim($_GET['host'] ?? ''));
|
||||
if (!preg_match('/^host\d+$/', $host)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit;
|
||||
}
|
||||
$script = SCRIPTS_DIR . '/Tools/remote_arr_cache_writer.sh';
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'remote_arr_cache_writer.sh not found']); exit;
|
||||
}
|
||||
set_time_limit(30);
|
||||
$out = []; $exit = 0;
|
||||
exec('bash ' . escapeshellarg($script) . ' --host=' . escapeshellarg(strtoupper($host)) . ' 2>&1', $out, $exit);
|
||||
|
||||
$cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $host . '.json';
|
||||
$node = null;
|
||||
if (file_exists($cacheFile)) {
|
||||
$node = json_decode(file_get_contents($cacheFile), true) ?: null;
|
||||
if ($node) {
|
||||
$node['cached'] = true;
|
||||
$node['cache_age'] = time() - (int)filemtime($cacheFile);
|
||||
}
|
||||
}
|
||||
// Bust the main arrs cache so next poll gets fresh data
|
||||
@unlink(VV_CACHE_DIR . '/arrs.json');
|
||||
echo json_encode(['ok' => $exit === 0, 'node' => $node, 'output' => implode("\n", $out)]);
|
||||
exit;
|
||||
}
|
||||
unset($_action);
|
||||
|
||||
// ── Normal data load ──────────────────────────────────────────────────────────
|
||||
$_vv_cached = vv_cache_read('arrs', 90);
|
||||
if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; }
|
||||
unset($_vv_cached);
|
||||
|
||||
@@ -240,32 +240,61 @@ function vv_arr_recovery_stats(): array {
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ── Local node only — called via SSH by remote_arr_cache_writer.sh on remote hosts ───────────
|
||||
|
||||
function vv_arrs_local_node(): array {
|
||||
$host = vv_detect_host();
|
||||
$names = vv_arr_node_names();
|
||||
$arrs = [];
|
||||
|
||||
foreach (vv_discover_arrs() as $node) {
|
||||
if ($node['host'] !== $host) continue;
|
||||
foreach ($node['arrs'] as $arr) {
|
||||
$entry = ['type' => $arr['type'], 'root' => $arr['root']];
|
||||
$entry = array_merge($entry, vv_fetch_arr_live($arr));
|
||||
$entry['cleanup'] = vv_arr_cleanup_stats($arr['type']);
|
||||
$entry['discovery'] = vv_arr_discovery_stats($arr['type']);
|
||||
$arrs[] = $entry;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
'host' => $host,
|
||||
'name' => $names[$host] ?? strtoupper($host),
|
||||
'local' => true,
|
||||
'arrs' => $arrs,
|
||||
];
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
function vv_arrs_all(): array {
|
||||
$currentHost = vv_detect_host();
|
||||
$names = vv_arr_node_names();
|
||||
$allNodes = vv_discover_arrs();
|
||||
$result = [];
|
||||
|
||||
foreach ($allNodes as $node) {
|
||||
$h = $node['host'];
|
||||
$isLocal = ($h === $currentHost || $currentHost === 'unknown');
|
||||
$nodeOut = ['host' => $h, 'name' => $names[$h] ?? strtoupper($h), 'local' => $isLocal, 'arrs' => []];
|
||||
// Local node — full live data
|
||||
$result = [vv_arrs_local_node()];
|
||||
|
||||
foreach ($node['arrs'] as $arr) {
|
||||
$entry = ['type' => $arr['type'], 'root' => $arr['root']];
|
||||
if ($isLocal) {
|
||||
$entry = array_merge($entry, vv_fetch_arr_live($arr));
|
||||
$entry['cleanup'] = vv_arr_cleanup_stats($arr['type']);
|
||||
$entry['discovery'] = vv_arr_discovery_stats($arr['type']);
|
||||
} else {
|
||||
$entry['online'] = false;
|
||||
$entry['remote'] = true;
|
||||
}
|
||||
$nodeOut['arrs'][] = $entry;
|
||||
// Remote nodes — read from file cache written by remote_arr_cache_writer.sh
|
||||
foreach (array_keys($names) as $h) {
|
||||
if ($h === $currentHost) continue;
|
||||
$cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $h . '.json';
|
||||
if (file_exists($cacheFile)) {
|
||||
$node = json_decode(file_get_contents($cacheFile), true) ?: [];
|
||||
$node['cached'] = true;
|
||||
$node['cache_age'] = time() - (int)filemtime($cacheFile);
|
||||
$result[] = $node;
|
||||
} else {
|
||||
$result[] = [
|
||||
'host' => $h,
|
||||
'name' => $names[$h],
|
||||
'local' => false,
|
||||
'cached' => false,
|
||||
'cache_miss' => true,
|
||||
'arrs' => [],
|
||||
];
|
||||
}
|
||||
$result[] = $nodeOut;
|
||||
}
|
||||
|
||||
$vars = vv_conf_vars();
|
||||
|
||||
@@ -248,16 +248,41 @@ function _arrCard(arr) {
|
||||
}
|
||||
|
||||
// ── Node section ──────────────────────────────────────────────────────────────
|
||||
function _nodeSection(node, ownerHost) {
|
||||
function _nodeSection(node) {
|
||||
const isLocal = node.local;
|
||||
const tag = isLocal ? '<span class="vv-arr-node-tag">local</span>' : '';
|
||||
const cards = node.arrs.map(_arrCard).join('');
|
||||
const isMiss = node.cache_miss;
|
||||
|
||||
let tag, body;
|
||||
|
||||
if (isLocal) {
|
||||
tag = '<span class="vv-arr-node-tag">local</span>';
|
||||
body = `<div class="vv-arr-cards">${node.arrs.map(_arrCard).join('')}</div>`;
|
||||
} else if (isMiss) {
|
||||
tag = `<span class="vv-arr-node-tag" style="background:#111;border-color:#222;color:#333;">no data</span>
|
||||
<button id="vv-arr-rfsh-${node.host}" onclick="vvArrsRefreshRemote('${node.host}')"
|
||||
style="font-size:9px;color:#4a9eff;background:#0a1a2a;border:1px solid #1a3a5a;
|
||||
border-radius:3px;padding:1px 8px;cursor:pointer;margin-left:6px;">↻ Fetch now</button>`;
|
||||
body = `<div style="color:#333;font-size:11px;padding:10px 0;">
|
||||
No remote data yet — first fetch runs within 2 hours, or click ↻ Fetch now.
|
||||
</div>`;
|
||||
} else {
|
||||
const age = node.cache_age || 0;
|
||||
const ageStr = age < 3600 ? Math.floor(age/60) + 'm ago'
|
||||
: age < 86400 ? Math.floor(age/3600) + 'h ago'
|
||||
: Math.floor(age/86400) + 'd ago';
|
||||
tag = `<span class="vv-arr-node-tag" style="background:#1a1000;border-color:#3a2800;color:#b87;">cached ${ageStr}</span>
|
||||
<button id="vv-arr-rfsh-${node.host}" onclick="vvArrsRefreshRemote('${node.host}')"
|
||||
style="font-size:9px;color:#666;background:none;border:1px solid #2a2a2a;
|
||||
border-radius:3px;padding:1px 8px;cursor:pointer;margin-left:6px;">↻</button>`;
|
||||
body = `<div class="vv-arr-cards" style="opacity:.85;">${node.arrs.map(_arrCard).join('')}</div>`;
|
||||
}
|
||||
|
||||
return `<div class="vv-arr-node">
|
||||
<div class="vv-arr-node-hdr">
|
||||
<span class="vv-arr-node-id">${node.host.toUpperCase()}</span>
|
||||
<span class="vv-arr-node-host">${node.name}</span>${tag}
|
||||
</div>
|
||||
<div class="vv-arr-cards">${cards}</div>
|
||||
${body}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -372,7 +397,7 @@ function _render(data) {
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const node of nodes) html += _nodeSection(node, data.host);
|
||||
for (const node of nodes) html += _nodeSection(node);
|
||||
html += _syncSection(data.sync || {}, data.recovery || {}, data.settings);
|
||||
html += _settingsCard(data.settings);
|
||||
|
||||
@@ -393,6 +418,20 @@ setInterval(vvArrsLoad, 60000);
|
||||
|
||||
})();
|
||||
|
||||
// ── Remote node refresh ───────────────────────────────────────────────────────
|
||||
function vvArrsRefreshRemote(host) {
|
||||
const btn = document.getElementById('vv-arr-rfsh-' + host);
|
||||
if (btn) { btn.disabled = true; btn.textContent = '↻…'; }
|
||||
fetch(`/plugins/varaverk/api/arrs.php?action=refresh_remote&host=${host}&_=` + Date.now())
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (btn) { btn.disabled = false; btn.textContent = d.ok ? '↻' : '✗'; }
|
||||
// Main cache was busted — reload to show fresh data
|
||||
setTimeout(vvArrsLoad, 500);
|
||||
})
|
||||
.catch(() => { if (btn) { btn.disabled = false; btn.textContent = '↻'; } });
|
||||
}
|
||||
|
||||
// ── Toggle handler ────────────────────────────────────────────────────────────
|
||||
function vvArrToggle(track, key, file) {
|
||||
const on = !track.classList.contains('on');
|
||||
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ============================= Remote Arr Cache Writer ========================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# SSHes to each remote partner host, calls vv_arrs_local_node() on their PHP
|
||||
# stack, and caches the result locally in /tmp/vv_cache/arrs_remote_hostN.json.
|
||||
#
|
||||
# The arrs page reads these files for instant initial load without hitting the
|
||||
# remote arr APIs on every page view. This script runs every 2 hours so remote
|
||||
# library counts stay reasonably current without hammering the network.
|
||||
#
|
||||
# Accepts --host=HOST2 to refresh a single host (used by the UI refresh button).
|
||||
#
|
||||
# ==============================================================================================
|
||||
# USAGE
|
||||
# ==============================================================================================
|
||||
#
|
||||
# remote_arr_cache_writer.sh — refresh all remote hosts
|
||||
# remote_arr_cache_writer.sh --host=HOST2 — refresh one host only
|
||||
# remote_arr_cache_writer.sh --dry-run — show what would happen
|
||||
# remote_arr_cache_writer.sh --log — verbose output
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
acquire_lock
|
||||
detect_hosts
|
||||
|
||||
# Parse --host= from raw args
|
||||
TARGET_HOST=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in --host=*) TARGET_HOST="${arg#--host=}" ;; esac
|
||||
done
|
||||
|
||||
mkdir -p /tmp/vv_cache
|
||||
|
||||
FETCH_OK=0
|
||||
FETCH_FAIL=0
|
||||
FETCH_SKIP=0
|
||||
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Remote Arr Cache Writer ━━━"
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||
[[ -n "$TARGET_HOST" ]] && echo " Target: $TARGET_HOST"
|
||||
echo ""
|
||||
|
||||
for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do
|
||||
[[ "$host_var" == "$MY_ID" ]] && continue
|
||||
|
||||
hostname="${!host_var:-}"
|
||||
[[ -z "$hostname" ]] && continue
|
||||
|
||||
# If targeting a specific host, skip others
|
||||
if [[ -n "$TARGET_HOST" ]]; then
|
||||
[[ "${host_var,,}" != "${TARGET_HOST,,}" && "$host_var" != "$TARGET_HOST" ]] && continue
|
||||
fi
|
||||
|
||||
host_id="${host_var,,}" # host1, host2, …
|
||||
cache_file="/tmp/vv_cache/arrs_remote_${host_id}.json"
|
||||
|
||||
echo " $host_var ($hostname)…"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn " DRY RUN — would SSH to $hostname and cache arr data"
|
||||
(( FETCH_SKIP++ ))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Resolve Tailscale IP
|
||||
REMOTE_IP=$(resolve_tailscale_ip "$hostname")
|
||||
if [[ -z "$REMOTE_IP" ]]; then
|
||||
warn " $host_var: cannot resolve Tailscale IP for $hostname — skipping"
|
||||
(( FETCH_FAIL++ ))
|
||||
continue
|
||||
fi
|
||||
log " $host_var: resolved $hostname → $REMOTE_IP"
|
||||
|
||||
# SSH and call vv_arrs_local_node() on the remote
|
||||
RESULT=$(ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout=10 \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o BatchMode=yes \
|
||||
"root@${REMOTE_IP}" \
|
||||
"php -r \"require_once '/usr/local/emhttp/plugins/varaverk/include/arrs.php'; echo json_encode(vv_arrs_local_node());\"" \
|
||||
2>/dev/null)
|
||||
|
||||
if [[ -z "$RESULT" ]]; then
|
||||
warn " $host_var: empty response from SSH — skipping"
|
||||
(( FETCH_FAIL++ ))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Validate JSON
|
||||
if ! echo "$RESULT" | php -r "
|
||||
\$d = json_decode(file_get_contents('php://stdin'), true);
|
||||
exit(!is_array(\$d) || !isset(\$d['arrs']) ? 1 : 0);
|
||||
" 2>/dev/null; then
|
||||
warn " $host_var: invalid or unexpected JSON — skipping"
|
||||
log " Response: ${RESULT:0:200}"
|
||||
(( FETCH_FAIL++ ))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$RESULT" > "$cache_file"
|
||||
CACHED_TYPES=$(echo "$RESULT" | php -r "
|
||||
\$d = json_decode(file_get_contents('php://stdin'), true);
|
||||
echo implode(', ', array_column(\$d['arrs'] ?? [], 'type'));
|
||||
" 2>/dev/null)
|
||||
echo " $host_var: cached ✅ — ${CACHED_TYPES:-no arrs}"
|
||||
(( FETCH_OK++ ))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY Cache Writer Summary ━━━━━"
|
||||
echo " $FETCH_OK updated · $FETCH_FAIL failed · $FETCH_SKIP skipped"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
Reference in New Issue
Block a user