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), ]; }