audit echo vs log across all scripts — outcomes always visible, verbose for per-item loops
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
// Arr (Sonarr / Radarr / Lidarr) data helpers
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
// ── Conf helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function vv_arr_scalar(string $raw, string $key): string {
|
||||
return vv_parse_conf_scalar($raw, $key);
|
||||
}
|
||||
|
||||
function vv_arr_known_hosts(): array {
|
||||
return vv_known_hosts();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ── 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();
|
||||
|
||||
// Local node — full live data
|
||||
$result = [vv_arrs_local_node()];
|
||||
|
||||
// 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' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$vars = vv_conf_vars();
|
||||
$myId = strtoupper($currentHost);
|
||||
|
||||
return [
|
||||
'nodes' => $result,
|
||||
'sync' => vv_arr_sync_stats(),
|
||||
'recovery' => vv_arr_recovery_stats(),
|
||||
'host' => $currentHost,
|
||||
'settings' => [
|
||||
'arr_sync_enabled' => ($vars['ARR_SYNC_ENABLED'] ?? 'true') !== 'false',
|
||||
'sonarr_recovery' => ($vars[$myId . '_SONARR_RECOVERY'] ?? 'true') !== 'false',
|
||||
'radarr_recovery' => ($vars[$myId . '_RADARR_RECOVERY'] ?? 'true') !== 'false',
|
||||
'lidarr_recovery' => ($vars[$myId . '_LIDARR_RECOVERY'] ?? 'true') !== 'false',
|
||||
'recovery_age_hours' => (int)($vars['ARR_IMPORT_RECOVERY_AGE'] ?? 6),
|
||||
'sonarr_port' => (int)($vars['ARR_SYNC_SONARR_PORT'] ?? 8989),
|
||||
'radarr_port' => (int)($vars['ARR_SYNC_RADARR_PORT'] ?? 7878),
|
||||
'lidarr_port' => (int)($vars['ARR_SYNC_LIDARR_PORT'] ?? 8686),
|
||||
'my_host' => $currentHost,
|
||||
'my_id' => $myId,
|
||||
],
|
||||
'ts' => time(),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user