378 lines
15 KiB
PHP
378 lines
15 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 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_map(
|
|
fn($x) => $x['statistics']['episodeFileCount'] ?? $x['episodeFileCount'] ?? 0, $data));
|
|
}
|
|
} 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)) {
|
|
$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)) {
|
|
$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]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: daily aggregate db — date|arr|orphan_count|orphan_bytes|junk_count|junk_bytes|recent_count|tracked_count
|
|
if ($out['last_run'] === null) {
|
|
$dbFile = DATA_DIR . '/arr_cleanup_stats.db';
|
|
if (file_exists($dbFile)) {
|
|
$last = null;
|
|
foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
|
$p = explode('|', $line);
|
|
if (count($p) >= 8 && $p[1] === $type) $last = $p;
|
|
}
|
|
if ($last) {
|
|
$out['last_run'] = strtotime($last[0] . ' 23:59:00') ?: null;
|
|
$out['status'] = 'ok';
|
|
$out['orphans'] = (int)$last[2];
|
|
$out['junk'] = (int)$last[4];
|
|
$out['tracked'] = (int)$last[7];
|
|
}
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
$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];
|
|
}
|
|
}
|
|
|
|
// Fallback: per-title history db — status|id|date[|title]
|
|
if ($out['last_run'] === null) {
|
|
$dbFile = DATA_DIR . '/' . $type . '_discovery_history.db';
|
|
if (file_exists($dbFile)) {
|
|
$lastDate = null; $added = 0;
|
|
foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
|
$p = explode('|', $line);
|
|
if (count($p) < 3) continue;
|
|
$date = $p[2];
|
|
if ($date !== $lastDate) { $lastDate = $date; $added = 0; }
|
|
if ($p[0] === 'ACCEPT') $added++;
|
|
}
|
|
if ($lastDate) {
|
|
$out['last_run'] = strtotime($lastDate . ' 23:59:00') ?: null;
|
|
$out['status'] = 'ok';
|
|
$out['added'] = $added;
|
|
}
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
$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];
|
|
}
|
|
}
|
|
|
|
// Fallback: daily aggregate db — date|time|count|bytes
|
|
if ($out['last_run'] === null) {
|
|
$dbFile = DATA_DIR . '/arr_recovery_stats.db';
|
|
if (file_exists($dbFile)) {
|
|
$lines = file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
$last = $lines ? end($lines) : null;
|
|
if ($last) {
|
|
$p = explode('|', $last);
|
|
if (count($p) >= 3) {
|
|
$ts = strtotime(($p[0] ?? '') . ' ' . ($p[1] ?? '00:00')) ?: null;
|
|
if ($ts) {
|
|
$out['last_run'] = $ts;
|
|
$out['status'] = 'ok';
|
|
$out['fixed'] = (int)($p[2] ?? 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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(),
|
|
];
|
|
}
|