- FallBack tab: per-node tier inventory + active fallback card with duration, tier, handback strikes, running container status - Watchdog tab: live system health (RAM bar + thresholds, load, uptime, daemon), docker watchdog strikes + skip list + restart history, stability strikes + reboot log, resource pressure alert card, config inventory (mem limits, required, pause/stop lists) - Swapped partnership/arrs tab order; FallBack between partnership and watchdog - Plugin source tree moved from Plugin/usr/local/emhttp/plugins/varaverk/ to Plugin/unraid/ - Deployment/ conf templates added
279 lines
11 KiB
PHP
279 lines
11 KiB
PHP
<?php
|
|
// Arr (Sonarr / Radarr / Lidarr) data helpers
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
|
|
// ── Conf helpers ──────────────────────────────────────────────────────────────
|
|
|
|
function vv_arr_scalar(string $raw, string $key): string {
|
|
return preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m)
|
|
? trim($m[1]) : '';
|
|
}
|
|
|
|
// Returns all configured hosts found in master.conf as ['host1'=>'hostname', ...]
|
|
// Returns ['host1' => 'hostname', 'host2' => 'hostname', ...] from master.conf.
|
|
// Matches both HOST1="name" and HOST1_NAME="name" (either convention).
|
|
function vv_arr_known_hosts(): array {
|
|
$vars = vv_conf_vars(); // already parses HOST1="val" correctly
|
|
$hosts = [];
|
|
foreach ($vars as $k => $v) {
|
|
if (preg_match('/^HOST(\d+)$/', $k, $m) && $v !== '') {
|
|
$hosts['host' . $m[1]] = $v;
|
|
}
|
|
}
|
|
ksort($hosts);
|
|
return $hosts ?: ['host1' => 'HOST1'];
|
|
}
|
|
|
|
function vv_arr_node_names(): array {
|
|
return array_map(fn($name) => $name, vv_arr_known_hosts());
|
|
}
|
|
|
|
// ── Discovery ─────────────────────────────────────────────────────────────────
|
|
|
|
function vv_discover_arrs(): array {
|
|
$nodes = [];
|
|
$defs = [
|
|
'sonarr' => ['SONARR_URL', 'SONARR_API_KEY', 'SONARR_TV_ROOT', 'v3'],
|
|
'radarr' => ['RADARR_URL', 'RADARR_API_KEY', 'RADARR_MOVIES_ROOT', 'v3'],
|
|
'lidarr' => ['LIDARR_URL', 'LIDARR_API_KEY', 'LIDARR_MUSIC_ROOT', 'v1'],
|
|
];
|
|
foreach (array_keys(vv_arr_known_hosts()) as $h) {
|
|
$raw = vv_read_conf_raw($h . '.conf');
|
|
if (!$raw) continue;
|
|
$pfx = strtoupper($h) . '_';
|
|
$get = fn($k) => vv_arr_scalar($raw, $pfx . $k);
|
|
$arrs = [];
|
|
foreach ($defs as $type => [$uk, $ak, $rk, $api]) {
|
|
$url = $get($uk);
|
|
$key = $get($ak);
|
|
if ($url && $key && !str_contains($key, 'your-')) {
|
|
$arrs[] = ['type' => $type, 'url' => $url, 'key' => $key,
|
|
'root' => $get($rk), 'api' => $api];
|
|
}
|
|
}
|
|
if ($arrs) $nodes[] = ['host' => $h, 'arrs' => $arrs];
|
|
}
|
|
return $nodes;
|
|
}
|
|
|
|
// ── HTTP ──────────────────────────────────────────────────────────────────────
|
|
|
|
function vv_arr_http(string $url, string $apiKey, string $path, int $timeout = 4): ?array {
|
|
$ctx = stream_context_create(['http' => [
|
|
'timeout' => $timeout,
|
|
'header' => "X-Api-Key: $apiKey\r\nAccept: application/json\r\n",
|
|
'ignore_errors' => true,
|
|
]]);
|
|
$raw = @file_get_contents(rtrim($url, '/') . $path, false, $ctx);
|
|
return $raw ? (json_decode($raw, true) ?: null) : null;
|
|
}
|
|
|
|
// ── Live arr data ─────────────────────────────────────────────────────────────
|
|
|
|
function vv_fetch_arr_live(array $arr): array {
|
|
$url = $arr['url'];
|
|
$key = $arr['key'];
|
|
$base = '/api/' . $arr['api'];
|
|
$type = $arr['type'];
|
|
|
|
$out = ['online' => false, 'version' => null, 'health' => [],
|
|
'queue' => ['dl' => 0, 'warn' => 0, 'err' => 0], 'disk' => []];
|
|
|
|
$sys = vv_arr_http($url, $key, "$base/system/status");
|
|
if (!$sys) return $out;
|
|
$out['online'] = true;
|
|
$out['version'] = $sys['version'] ?? null;
|
|
|
|
if ($type === 'sonarr') {
|
|
$data = vv_arr_http($url, $key, "$base/series");
|
|
if (is_array($data)) {
|
|
$out['total'] = count($data);
|
|
$out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
|
|
$out['episodes'] = array_sum(array_column($data, 'episodeFileCount'));
|
|
}
|
|
} elseif ($type === 'radarr') {
|
|
$data = vv_arr_http($url, $key, "$base/movie");
|
|
if (is_array($data)) {
|
|
$out['total'] = count($data);
|
|
$out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
|
|
$out['files'] = count(array_filter($data, fn($x) => !empty($x['hasFile'])));
|
|
}
|
|
} elseif ($type === 'lidarr') {
|
|
$data = vv_arr_http($url, $key, "$base/artist");
|
|
if (is_array($data)) {
|
|
$out['total'] = count($data);
|
|
$out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
|
|
$out['albums'] = array_sum(array_map(
|
|
fn($a) => $a['statistics']['albumCount'] ?? $a['albumCount'] ?? 0, $data));
|
|
}
|
|
}
|
|
|
|
$q = vv_arr_http($url, $key, "$base/queue?page=1&pageSize=500");
|
|
if (is_array($q)) {
|
|
foreach (($q['records'] ?? $q) as $r) {
|
|
if (!is_array($r)) continue;
|
|
$s = $r['status'] ?? '';
|
|
$tds = strtolower($r['trackedDownloadStatus'] ?? '');
|
|
$tst = strtolower($r['trackedDownloadState'] ?? '');
|
|
if ($s === 'downloading') $out['queue']['dl']++;
|
|
if ($tds === 'warning' || $tst === 'downloadingstalled') $out['queue']['warn']++;
|
|
if ($tds === 'error') $out['queue']['err']++;
|
|
}
|
|
}
|
|
|
|
$h = vv_arr_http($url, $key, "$base/health");
|
|
if (is_array($h)) $out['health'] = $h;
|
|
|
|
$d = vv_arr_http($url, $key, "$base/diskspace");
|
|
if (is_array($d)) $out['disk'] = $d;
|
|
|
|
return $out;
|
|
}
|
|
|
|
// ── Log stats ─────────────────────────────────────────────────────────────────
|
|
|
|
function vv_arr_cleanup_stats(string $type): array {
|
|
$slugs = ['sonarr' => 'Media/sonarr_cleanup',
|
|
'radarr' => 'Media/radarr_cleanup',
|
|
'lidarr' => 'Media/lidarr_cleanup'];
|
|
$base = LOG_DIR . '/' . ($slugs[$type] ?? '');
|
|
$out = ['last_run' => null, 'end' => null, 'status' => null,
|
|
'tracked' => null, 'total' => null,
|
|
'orphans' => 0, 'orphans_sz' => '0B', 'junk' => 0];
|
|
|
|
$jf = $base . '.json';
|
|
if (!file_exists($jf)) return $out;
|
|
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
|
$out['last_run'] = $meta['start'] ?? null;
|
|
$out['end'] = $meta['end'] ?? null;
|
|
$out['status'] = $meta['status'] ?? null;
|
|
|
|
$lf = $base . '.log';
|
|
if (!file_exists($lf)) return $out;
|
|
$log = file_get_contents($lf);
|
|
$parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
|
|
$blk = count($parts) > 1 ? end($parts) : $log;
|
|
|
|
if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
|
|
$out['tracked'] = (int)str_replace(',', '', $m[1]);
|
|
$out['total'] = (int)str_replace(',', '', $m[2]);
|
|
}
|
|
if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
|
|
$out['orphans'] = (int)str_replace(',', '', $m[1]);
|
|
$out['orphans_sz'] = trim($m[2]);
|
|
}
|
|
if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
|
|
$out['junk'] = (int)str_replace(',', '', $m[1]);
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
function vv_arr_discovery_stats(string $type): array {
|
|
$slugs = ['sonarr' => 'Media/playback_aware_sonarr_discovery',
|
|
'radarr' => 'Media/playback_aware_radarr_discovery',
|
|
'lidarr' => 'Media/playback_aware_lidarr_discovery'];
|
|
$base = LOG_DIR . '/' . ($slugs[$type] ?? '');
|
|
$out = ['last_run' => null, 'status' => null, 'added' => null];
|
|
|
|
$jf = $base . '.json';
|
|
if (!file_exists($jf)) return $out;
|
|
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
|
$out['last_run'] = $meta['start'] ?? null;
|
|
$out['status'] = $meta['status'] ?? null;
|
|
|
|
$lf = $base . '.log';
|
|
if (file_exists($lf)) {
|
|
$log = file_get_contents($lf);
|
|
if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
|
|
elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
function vv_arr_sync_stats(): array {
|
|
$base = LOG_DIR . '/Media/arr_sync';
|
|
$out = ['last_run' => null, 'status' => null, 'added' => null,
|
|
'nodes' => null, 'blocklist_count' => null];
|
|
|
|
$jf = $base . '.json';
|
|
if (file_exists($jf)) {
|
|
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
|
$out['last_run'] = $meta['start'] ?? null;
|
|
$out['status'] = $meta['status'] ?? null;
|
|
}
|
|
|
|
$master = vv_read_conf_raw('master.conf');
|
|
if (preg_match('/ARR_SYNC_BLOCKLIST\s*=\s*"?([^"\n#]+)"?/m', $master, $m)) {
|
|
$blPath = trim($m[1]);
|
|
if (file_exists($blPath)) {
|
|
$out['blocklist_count'] = count(array_filter(
|
|
file($blPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)));
|
|
}
|
|
}
|
|
|
|
$lf = $base . '.log';
|
|
if (file_exists($lf)) {
|
|
$log = file_get_contents($lf);
|
|
if (preg_match('/Total added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
|
|
if (preg_match('/Nodes?[:\s]+(\d+)/i', $log, $m)) $out['nodes'] = (int)$m[1];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
function vv_arr_recovery_stats(): array {
|
|
$base = LOG_DIR . '/Media/arrs_failed_stalled_recovery';
|
|
$out = ['last_run' => null, 'status' => null, 'fixed' => 0, 'searched' => 0];
|
|
|
|
$jf = $base . '.json';
|
|
if (!file_exists($jf)) return $out;
|
|
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
|
$out['last_run'] = $meta['start'] ?? null;
|
|
$out['status'] = $meta['status'] ?? null;
|
|
|
|
$lf = $base . '.log';
|
|
if (file_exists($lf)) {
|
|
$log = file_get_contents($lf);
|
|
if (preg_match('/Removed[:\s]+(\d+)/i', $log, $m)) $out['fixed'] = (int)$m[1];
|
|
if (preg_match('/Re-searched[:\s]+(\d+)/i',$log, $m)) $out['searched'] = (int)$m[1];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
// ── 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' => []];
|
|
|
|
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;
|
|
}
|
|
$result[] = $nodeOut;
|
|
}
|
|
|
|
return [
|
|
'nodes' => $result,
|
|
'sync' => vv_arr_sync_stats(),
|
|
'recovery' => vv_arr_recovery_stats(),
|
|
'host' => $currentHost,
|
|
'ts' => time(),
|
|
];
|
|
}
|