query(" SELECT id, name, platform, kick_name, status, game, title FROM streamers ORDER BY name ")->fetchAll(); json_out($rows); } // Refresh request — admin only require_admin(); $cache_file = '/tmp/snb_live_cache.json'; $now = time(); // Return cached result if still fresh if (file_exists($cache_file)) { $cache = json_decode(file_get_contents($cache_file), true); if ($cache && ($now - $cache['ts']) < LIVE_CACHE_TTL) { json_out(['cached' => true, 'age' => $now - $cache['ts'], 'results' => $cache['results']]); } } // Fetch all streamers from DB $rows = db()->query(" SELECT id, name, platform, kick_name FROM streamers ORDER BY name ")->fetchAll(); $twitch_streamers = []; $kick_streamers = []; foreach ($rows as $row) { if ($row['platform'] === 'kick' && !empty($row['kick_name'])) { $kick_streamers[] = $row; } else { $twitch_streamers[] = $row; } } $results = []; // ------------------------------------------------------------------ // Twitch — Helix API // ------------------------------------------------------------------ if ($twitch_streamers) { $token = get_twitch_token(); if ($token) { foreach (array_chunk($twitch_streamers, 100) as $batch) { // Build URL with multiple login= params $params = array_map( fn($s) => 'user_login=' . urlencode(strtolower($s['name'])), $batch ); $url = 'https://api.twitch.tv/helix/streams?' . implode('&', $params) . '&first=100'; $response = twitch_get($url, $token); // Build lookup by login name $live = []; if ($response && isset($response['data'])) { foreach ($response['data'] as $stream) { $live[strtolower($stream['user_login'])] = $stream; } } foreach ($batch as $s) { $login = strtolower($s['name']); $stream = $live[$login] ?? null; $results[$s['id']] = [ 'id' => $s['id'], 'name' => $s['name'], 'status' => $stream ? 'live' : 'offline', 'game' => $stream['game_name'] ?? '', 'title' => $stream['title'] ?? '', ]; } } } else { foreach ($twitch_streamers as $s) { $results[$s['id']] = [ 'id' => $s['id'], 'name' => $s['name'], 'status' => 'unknown', 'game' => '', 'title' => '', ]; } } } // ------------------------------------------------------------------ // Kick — public API // ------------------------------------------------------------------ foreach ($kick_streamers as $s) { $kick_name = $s['kick_name'] ?: $s['name']; // Kick public channel API — no auth required $url = 'https://kick.com/api/v2/channels/' . urlencode(strtolower($kick_name)); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'Accept-Language: en-US,en;q=0.9', 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $data = ($status === 200 && $body) ? json_decode($body, true) : null; // livestream key is present and non-null when live $is_live = !empty($data['livestream']); $results[$s['id']] = [ 'id' => $s['id'], 'name' => $s['name'], 'status' => $is_live ? 'live' : 'offline', 'game' => $data['livestream']['categories'][0]['name'] ?? '', 'title' => $data['livestream']['session_title'] ?? '', ]; } // ------------------------------------------------------------------ // Write results back to DB // ------------------------------------------------------------------ if ($results) { $db = db(); $stmt = $db->prepare(" UPDATE streamers SET status = :status, game = :game, title = :title WHERE id = :id "); foreach ($results as $r) { if ($r['status'] === 'unknown') continue; $stmt->execute([ ':status' => $r['status'], ':game' => $r['game'], ':title' => $r['title'], ':id' => $r['id'], ]); } } // Cache file_put_contents($cache_file, json_encode([ 'ts' => $now, 'results' => array_values($results), ])); json_out(['cached' => false, 'results' => array_values($results)]); // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ function get_twitch_token(): ?string { $cache_file = '/tmp/snb_twitch_token.json'; if (file_exists($cache_file)) { $cached = json_decode(file_get_contents($cache_file), true); if ($cached && $cached['expires_at'] > time() + 3600) { return $cached['access_token']; } } $ch = curl_init('https://id.twitch.tv/oauth2/token'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query([ 'client_id' => TWITCH_CLIENT_ID, 'client_secret' => TWITCH_CLIENT_SECRET, 'grant_type' => 'client_credentials', ]), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) return null; $data = json_decode($body, true); if (empty($data['access_token'])) return null; file_put_contents($cache_file, json_encode([ 'access_token' => $data['access_token'], 'expires_at' => time() + ($data['expires_in'] ?? 3600), ])); return $data['access_token']; } function twitch_get(string $url, string $token): ?array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'Client-ID: ' . TWITCH_CLIENT_ID, 'Authorization: Bearer ' . $token, ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return ($status === 200) ? json_decode($body, true) : null; }