Consolidations (config.php gains 5 shared utilities): - vv_format_uptime() replaces 4 inline uptime-formatting blocks - vv_parse_conf_scalar() replaces vv_arr_scalar/vv_wd_scalar/vv_fb_scalar/vv_media_conf_scalar - vv_known_hosts() replaces vv_arr_known_hosts/vv_fb_known_hosts + inline parser in watchdog - vv_parse_kv_db() replaces inline key=value parsing in snapshot and monitor - vv_local_ip() replaces duplicate in docker_folders.php and inline in docker.php All module-level function names kept as thin aliases so call sites unchanged. Critical bug fixes: - api/system.php: added require_once config.php and POST-only guard (no auth on shutdown) - api/movescript.php + reorderarray.php: use vv_write_conf_raw (atomic) + vv_push_master_conf - api/snapshot.php: share /tmp/vv_cpu_stat.json with vv_cpu_per_core() instead of own state file Correctness: - vv_cpu_per_core() and vv_network_stats(): atomic tmp+rename for state files (concurrent poll safety) - ext_ip curl cache moved from /tmp/vv_ext_ip.cache to vv_cache_read/write (canonical cache dir) - monitor_remote.php + board.php + snapshot.php: all use vv_cache_read/write instead of ad-hoc /tmp files HTTP method guards added to write-only APIs that were missing them: - api/scheduler.php, conf_toggle.php, flag_toggle.php
213 lines
8.9 KiB
PHP
213 lines
8.9 KiB
PHP
<?php
|
|
// Media server session helpers — reads HOST*_EMBY_* / JELLYFIN_* / PLEX_* from host conf.
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
|
|
// ── Conf reader ───────────────────────────────────────────────────────────────
|
|
|
|
function vv_media_conf_scalar(string $raw, string $key): string {
|
|
return vv_parse_conf_scalar($raw, $key);
|
|
}
|
|
|
|
// ── Server list from host conf ────────────────────────────────────────────────
|
|
|
|
function vv_discover_media_servers(): array {
|
|
$host = vv_detect_host(); // 'host1', 'host2', 'unknown'
|
|
|
|
// For unknown (dev), try both host confs; otherwise read only the current host's file.
|
|
$hostSlots = $host !== 'unknown' ? [$host] : ['host1', 'host2'];
|
|
|
|
$servers = [];
|
|
foreach ($hostSlots as $h) {
|
|
$raw = vv_read_conf_raw($h . '.conf');
|
|
$prefix = strtoupper($h) . '_'; // HOST1_ or HOST2_
|
|
|
|
$get = fn(string $k) => vv_media_conf_scalar($raw, $prefix . $k);
|
|
|
|
// ── Emby ──────────────────────────────────────────────────────────────
|
|
$embyUrl = $get('EMBY_URL');
|
|
$embyKey = $get('EMBY_API_KEY');
|
|
if ($embyUrl && $embyKey && !str_contains($embyKey, 'your-')) {
|
|
$servers[] = [
|
|
'type' => 'emby',
|
|
'name' => $get('EMBY_CONTAINER') ?: 'Emby',
|
|
'url' => $embyUrl,
|
|
'key' => $embyKey,
|
|
];
|
|
}
|
|
|
|
// ── Jellyfin ──────────────────────────────────────────────────────────
|
|
$jfUrl = $get('JELLYFIN_URL');
|
|
$jfKey = $get('JELLYFIN_API_KEY');
|
|
if ($jfUrl && $jfKey && !str_contains($jfKey, 'your-')) {
|
|
$servers[] = [
|
|
'type' => 'jellyfin',
|
|
'name' => $get('JELLYFIN_CONTAINER') ?: 'Jellyfin',
|
|
'url' => $jfUrl,
|
|
'key' => $jfKey,
|
|
];
|
|
}
|
|
|
|
// ── Plex (optional) ───────────────────────────────────────────────────
|
|
$plexToken = $get('PLEX_TOKEN');
|
|
$plexUrl = $get('PLEX_URL') ?: 'http://localhost:32400';
|
|
if ($plexToken && !str_contains($plexToken, 'your-')) {
|
|
$servers[] = [
|
|
'type' => 'plex',
|
|
'name' => 'Plex',
|
|
'url' => $plexUrl,
|
|
'token' => $plexToken,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Deduplicate — same server can appear from multiple conf sources (unknown host, shared keys)
|
|
$seen = [];
|
|
$unique = [];
|
|
foreach ($servers as $s) {
|
|
$k = $s['type'] . '|' . ($s['url'] ?? $s['token'] ?? '');
|
|
if (!isset($seen[$k])) { $seen[$k] = true; $unique[] = $s; }
|
|
}
|
|
return $unique;
|
|
}
|
|
|
|
// ── Session fetchers ──────────────────────────────────────────────────────────
|
|
|
|
function vv_fetch_jf_sessions(array $srv): array {
|
|
$url = rtrim($srv['url'], '/') . '/Sessions?api_key=' . urlencode($srv['key']) . '&activeWithinSeconds=60';
|
|
$ctx = stream_context_create(['http' => ['timeout' => 3]]);
|
|
$raw = @file_get_contents($url, false, $ctx);
|
|
if (!$raw) return [];
|
|
$sessions = json_decode($raw, true);
|
|
if (!is_array($sessions)) return [];
|
|
|
|
$result = [];
|
|
foreach ($sessions as $s) {
|
|
if (empty($s['NowPlayingItem'])) continue;
|
|
$item = $s['NowPlayingItem'];
|
|
$ps = $s['PlayState'] ?? [];
|
|
$tc = $s['TranscodingInfo'] ?? null;
|
|
|
|
$type = $item['Type'] ?? '';
|
|
$title = $item['Name'] ?? 'Unknown';
|
|
if ($type === 'Episode' && !empty($item['SeriesName'])) {
|
|
$ep = sprintf('S%02dE%02d', $item['ParentIndexNumber'] ?? 0, $item['IndexNumber'] ?? 0);
|
|
$title = $item['SeriesName'] . ' ' . $ep;
|
|
}
|
|
|
|
$pos = (int)($ps['PositionTicks'] ?? 0);
|
|
$dur = (int)($item['RunTimeTicks'] ?? 0);
|
|
$pct = $dur > 0 ? min(100, (int)round($pos / $dur * 100)) : 0;
|
|
|
|
if ($tc) {
|
|
$vc = strtoupper($tc['VideoCodec'] ?? '');
|
|
$hw = !empty($tc['IsHardwareAcceleratedVideoDecoding']) ? ' HW' : '';
|
|
$method = 'Transcode' . ($vc ? " ($vc$hw)" : '');
|
|
} else {
|
|
$pm = $ps['PlayMethod'] ?? '';
|
|
$method = $pm === 'DirectStream' ? 'Direct Stream' : 'Direct Play';
|
|
}
|
|
|
|
// Source video stream — resolution and codec of the file being played
|
|
$videoStream = null;
|
|
foreach ($item['MediaStreams'] ?? [] as $ms) {
|
|
if (($ms['Type'] ?? '') === 'Video') { $videoStream = $ms; break; }
|
|
}
|
|
|
|
$result[] = [
|
|
'server' => $srv['name'],
|
|
'server_type' => $srv['type'],
|
|
'user' => $s['UserName'] ?? '?',
|
|
'title' => $title,
|
|
'type' => $type,
|
|
'client' => trim(($s['Client'] ?? '') . ' / ' . ($s['DeviceName'] ?? ''), ' /'),
|
|
'method' => $method,
|
|
'paused' => !empty($ps['IsPaused']),
|
|
'pct' => $pct,
|
|
'pos_sec' => (int)($pos / 10000000),
|
|
'dur_sec' => (int)($dur / 10000000),
|
|
'is_tc' => $tc !== null,
|
|
'height' => (int)($videoStream['Height'] ?? 0),
|
|
'codec' => strtolower($videoStream['Codec'] ?? ''),
|
|
];
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
function vv_fetch_plex_sessions(array $srv): array {
|
|
$url = rtrim($srv['url'], '/') . '/status/sessions?X-Plex-Token=' . urlencode($srv['token']);
|
|
$ctx = stream_context_create(['http' => ['timeout' => 3, 'header' => "Accept: application/json\r\n"]]);
|
|
$raw = @file_get_contents($url, false, $ctx);
|
|
if (!$raw) return [];
|
|
$data = json_decode($raw, true);
|
|
$items = $data['MediaContainer']['Metadata'] ?? [];
|
|
if (!is_array($items)) return [];
|
|
|
|
$result = [];
|
|
foreach ($items as $m) {
|
|
$type = strtolower($m['type'] ?? '');
|
|
$title = $m['title'] ?? 'Unknown';
|
|
if ($type === 'episode') {
|
|
$title = ($m['grandparentTitle'] ?? '') . ' S' . str_pad($m['parentIndex'] ?? 0, 2, '0', STR_PAD_LEFT)
|
|
. 'E' . str_pad($m['index'] ?? 0, 2, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
$dur = (int)($m['duration'] ?? 0);
|
|
$offset = (int)($m['viewOffset'] ?? 0);
|
|
$pct = $dur > 0 ? min(100, (int)round($offset / $dur * 100)) : 0;
|
|
|
|
$tcInfo = $m['TranscodeSession'] ?? null;
|
|
$isTc = $tcInfo !== null;
|
|
if ($isTc) {
|
|
$vc = strtoupper($tcInfo['videoCodec'] ?? '');
|
|
$hw = !empty($tcInfo['transcodeHwEncoding']) ? ' HW' : '';
|
|
$method = 'Transcode' . ($vc ? " ($vc$hw)" : '');
|
|
} else {
|
|
$method = 'Direct Play';
|
|
}
|
|
|
|
$player = $m['Player'] ?? [];
|
|
$media0 = $m['Media'][0] ?? [];
|
|
$plexH = (int)($media0['height'] ?? 0);
|
|
if (!$plexH && !empty($media0['videoResolution'])) {
|
|
$vr = strtolower($media0['videoResolution']);
|
|
$plexH = $vr === '4k' ? 2160 : (int)$vr;
|
|
}
|
|
$result[] = [
|
|
'server' => $srv['name'],
|
|
'server_type' => $srv['type'],
|
|
'user' => ($m['User']['title'] ?? '?'),
|
|
'title' => $title,
|
|
'type' => ucfirst($type),
|
|
'client' => trim(($player['product'] ?? '') . ' / ' . ($player['title'] ?? ''), ' /'),
|
|
'method' => $method,
|
|
'paused' => ($player['state'] ?? '') === 'paused',
|
|
'pct' => $pct,
|
|
'pos_sec' => (int)($offset / 1000),
|
|
'dur_sec' => (int)($dur / 1000),
|
|
'is_tc' => $isTc,
|
|
'height' => $plexH,
|
|
'codec' => strtolower($media0['videoCodec'] ?? ''),
|
|
];
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
// ── Public entry point ────────────────────────────────────────────────────────
|
|
|
|
function vv_media_sessions(): array {
|
|
$servers = vv_discover_media_servers();
|
|
$sessions = [];
|
|
foreach ($servers as $srv) {
|
|
$found = $srv['type'] === 'plex'
|
|
? vv_fetch_plex_sessions($srv)
|
|
: vv_fetch_jf_sessions($srv);
|
|
foreach ($found as $s) $sessions[] = $s;
|
|
}
|
|
return [
|
|
'sessions' => $sessions,
|
|
'server_names' => array_column($servers, 'name'),
|
|
'server_count' => count($servers),
|
|
];
|
|
}
|