withOptions(['verify' => false]) ->head($streamUrl); if ($response->successful()) { return true; } $response = Http::timeout(2) ->withOptions(['verify' => false]) ->get($streamUrl); return $response->successful(); } catch (\Exception) { return false; } } public function getNowPlaying(?string $apiUrl): array { if (! $apiUrl) { return ['enabled' => true, 'song' => null, 'artist' => null]; } try { $response = Http::timeout(5)->get($apiUrl); if ($response->successful()) { $data = $response->json(); if (isset($data['now_playing'])) { $song = $data['now_playing']['song'] ?? $data['now_playing']; return [ 'enabled' => true, 'song' => $song['title'] ?? $song['text'] ?? null, 'artist' => $song['artist'] ?? null, ]; } return [ 'enabled' => true, 'song' => $data['song'] ?? $data['title'] ?? $data['now_playing'] ?? null, 'artist' => $data['artist'] ?? null, ]; } } catch (\Exception) { // Silent fail } return ['enabled' => true, 'song' => null, 'artist' => null]; } public function getListenersCount(?string $apiUrl): int { if (! $apiUrl) { return 0; } try { $response = Http::timeout(5)->get($apiUrl); if ($response->successful()) { $data = $response->json(); return $data['listeners']['total'] ?? $data['listeners']['current'] ?? $data['listeners'] ?? $data['count'] ?? $data['total'] ?? 0; } } catch (\Exception) { // Silent fail } return 0; } public function formatStreamUrl(string $url): string { if ($url === '' || $url === '0') { return $url; } $url = str_replace('http://', 'https://', $url); if (preg_match('/^(https?:\/\/[^\/]+):(\d+)\/(.+)$/', $url, $matches)) { $baseUrl = $matches[1]; $port = $matches[2]; $path = $matches[3]; if (in_array($port, ['8000', '8010', '8020', '8030', '8040', '8050'])) { return $baseUrl . '/radio/' . $port . '/' . $path; } } return $url; } public function detectStreamType(string $streamUrl): array { if (empty($streamUrl)) { return ['type' => 'unknown', 'detected' => false]; } $parsed = parse_url($streamUrl); if (! $parsed) { return ['type' => 'unknown', 'detected' => false]; } $scheme = $parsed['scheme'] ?? 'https'; $host = $parsed['host'] ?? ''; $baseUrl = $scheme . '://' . $host; // Try AzureCast first $azureCast = $this->tryAzureCast($baseUrl); if ($azureCast['detected']) { return $azureCast; } // Try Icecast $icecast = $this->tryIcecast($baseUrl); if ($icecast['detected']) { return $icecast; } // Try Shoutcast $shoutcast = $this->tryShoutcast($baseUrl, $streamUrl); if ($shoutcast['detected']) { return $shoutcast; } return ['type' => 'unknown', 'detected' => false, 'base_url' => $baseUrl]; } public function tryAzureCast(string $baseUrl): array { try { $response = Http::timeout(3)->get(rtrim($baseUrl, '/') . '/api/nowplaying'); if ($response->successful()) { $data = $response->json(); if (is_array($data) && (isset($data[0]['station']) || isset($data['station']))) { $stationId = $data[0]['station']['id'] ?? $data['station']['id'] ?? 1; return [ 'type' => 'azurecast', 'detected' => true, 'base_url' => $baseUrl, 'station_id' => $stationId, 'now_playing_api' => rtrim($baseUrl, '/') . '/api/nowplaying/' . $stationId, 'listeners_api' => rtrim($baseUrl, '/') . '/api/nowplaying/' . $stationId, ]; } } } catch (\Exception) { // Not AzureCast } return ['detected' => false]; } public function tryIcecast(string $baseUrl): array { try { $response = Http::timeout(3)->get(rtrim($baseUrl, '/') . '/status-json.xsl'); if ($response->successful()) { $data = $response->json(); if (isset($data['icestats'])) { $source = $data['icestats']['source'] ?? []; $listeners = 0; $song = null; $artist = null; if (is_array($source)) { if (isset($source['listeners'])) { $listeners = (int) $source['listeners']; $title = $source['title'] ?? ''; } else { $firstSource = $source[0] ?? null; if ($firstSource) { $listeners = (int) ($firstSource['listeners'] ?? 0); $title = $firstSource['title'] ?? ''; } } } $title = $title ?? ''; if (str_contains($title, ' - ')) { $parts = explode(' - ', $title, 2); $artist = trim($parts[0]); $song = trim($parts[1]); } else { $song = $title ?: null; } return [ 'type' => 'icecast', 'detected' => true, 'base_url' => $baseUrl, 'now_playing_api' => rtrim($baseUrl, '/') . '/status-json.xsl', 'listeners_api' => rtrim($baseUrl, '/') . '/status-json.xsl', 'listeners' => $listeners, 'song' => $song, 'artist' => $artist, ]; } } } catch (\Exception) { // Not Icecast } return ['detected' => false]; } public function tryShoutcast(string $baseUrl, string $streamUrl): array { // Try SHOUTcast stats endpoint $statsUrls = [ rtrim($baseUrl, '/') . '/stats?json=1', rtrim($baseUrl, '/') . '/7.html', ]; foreach ($statsUrls as $statsUrl) { try { $response = Http::timeout(3)->get($statsUrl); if ($response->successful()) { $body = $response->body(); if (str_contains($statsUrl, 'json=1')) { $data = json_decode($body, true); if ($data && isset($data['streamstatus'])) { $song = $data['songtitle'] ?? null; $artist = null; if ($song && str_contains($song, ' - ')) { $parts = explode(' - ', $song, 2); $artist = trim($parts[0]); $song = trim($parts[1]); } return [ 'type' => 'shoutcast', 'detected' => true, 'base_url' => $baseUrl, 'now_playing_api' => rtrim($baseUrl, '/') . '/stats?json=1', 'listeners_api' => rtrim($baseUrl, '/') . '/stats?json=1', 'listeners' => (int) ($data['currentlisteners'] ?? 0), 'song' => $song, 'artist' => $artist, 'stream_url' => $data['streamurl'] ?? $streamUrl, ]; } } if (str_contains($body, ',')) { $parts = explode(',', $body); if (count($parts) >= 7) { $listeners = (int) trim($parts[2]); return [ 'type' => 'shoutcast', 'detected' => true, 'base_url' => $baseUrl, 'now_playing_api' => rtrim($baseUrl, '/') . '/7.html', 'listeners_api' => rtrim($baseUrl, '/') . '/7.html', 'listeners' => $listeners, 'stream_url' => $streamUrl, ]; } } } } catch (\Exception) { continue; } } return ['detected' => false]; } public function testStreamConnection(string $streamUrl, ?string $nowPlayingApiUrl = null, ?string $listenersApiUrl = null): array { $results = [ 'stream' => ['status' => 'untested', 'message' => ''], 'now_playing' => ['status' => 'untested', 'message' => ''], 'listeners' => ['status' => 'untested', 'message' => ''], 'stream_type' => 'unknown', 'stream_info' => null, ]; if (empty($streamUrl)) { $results['stream'] = ['status' => 'error', 'message' => 'Geen stream URL opgegeven']; return $results; } // Test stream URL try { $response = Http::timeout(5) ->withOptions(['verify' => false]) ->head($streamUrl); if ($response->successful()) { $contentType = $response->header('Content-Type'); $results['stream'] = [ 'status' => 'success', 'message' => 'Stream bereikbaar!', 'content_type' => $contentType, 'http_code' => $response->status(), ]; } else { $results['stream'] = [ 'status' => 'warning', 'message' => 'Stream reageert met status ' . $response->status(), 'http_code' => $response->status(), ]; } } catch (\Exception $e) { $results['stream'] = [ 'status' => 'error', 'message' => 'Kon geen verbinding maken: ' . $e->getMessage(), ]; } // Detect stream type $detected = $this->detectStreamType($streamUrl); $results['stream_type'] = $detected['type']; $results['stream_info'] = $detected; // Test now-playing API $npUrl = $nowPlayingApiUrl ?: ($detected['now_playing_api'] ?? null); if ($npUrl) { try { $npResponse = Http::timeout(5)->get($npUrl); if ($npResponse->successful()) { $npData = $npResponse->json(); $song = null; $artist = null; if ($detected['type'] === 'azurecast') { $np = $npData['now_playing'] ?? []; $song = $np['song']['title'] ?? null; $artist = $np['song']['artist'] ?? null; } elseif ($detected['type'] === 'icecast') { $source = $npData['icestats']['source'] ?? []; if (is_array($source)) { if (isset($source['title'])) { $title = $source['title']; } else { $title = $source[0]['title'] ?? null; } if ($title && str_contains($title, ' - ')) { $parts = explode(' - ', $title, 2); $artist = trim($parts[0]); $song = trim($parts[1]); } else { $song = $title; } } } elseif ($detected['type'] === 'shoutcast') { $song = $npData['songtitle'] ?? null; if ($song && str_contains($song, ' - ')) { $parts = explode(' - ', $song, 2); $artist = trim($parts[0]); $song = trim($parts[1]); } } else { $song = $npData['song'] ?? $npData['title'] ?? null; $artist = $npData['artist'] ?? null; } $results['now_playing'] = [ 'status' => 'success', 'message' => 'Nu afspelen informatie beschikbaar', 'song' => $song, 'artist' => $artist, 'api_url' => $npUrl, ]; } else { $results['now_playing'] = [ 'status' => 'warning', 'message' => 'API reageert met status ' . $npResponse->status(), 'api_url' => $npUrl, ]; } } catch (\Exception $e) { $results['now_playing'] = [ 'status' => 'error', 'message' => 'Kon now-playing API niet bereiken: ' . $e->getMessage(), 'api_url' => $npUrl, ]; } } else { $results['now_playing'] = [ 'status' => 'skipped', 'message' => 'Geen now-playing API geconfigureerd', ]; } // Test listeners API $listenersUrl = $listenersApiUrl ?: ($detected['listeners_api'] ?? null); if ($listenersUrl) { try { $listenersResponse = Http::timeout(5)->get($listenersUrl); if ($listenersResponse->successful()) { $listenersData = $listenersResponse->json(); $count = 0; if ($detected['type'] === 'azurecast') { $count = $listenersData['listeners']['total'] ?? $listenersData['listeners']['current'] ?? 0; } elseif ($detected['type'] === 'icecast') { $source = $listenersData['icestats']['source'] ?? []; if (is_array($source)) { $count = (int) ($source['listeners'] ?? $source[0]['listeners'] ?? 0); } } elseif ($detected['type'] === 'shoutcast') { $count = (int) ($listenersData['currentlisteners'] ?? 0); } else { $count = $listenersData['listeners'] ?? $listenersData['count'] ?? $listenersData['total'] ?? 0; } $results['listeners'] = [ 'status' => 'success', 'message' => 'Luisteraars informatie beschikbaar', 'count' => (int) $count, 'api_url' => $listenersUrl, ]; } else { $results['listeners'] = [ 'status' => 'warning', 'message' => 'Listeners API reageert met status ' . $listenersResponse->status(), 'api_url' => $listenersUrl, ]; } } catch (\Exception $e) { $results['listeners'] = [ 'status' => 'error', 'message' => 'Kon listeners API niet bereiken: ' . $e->getMessage(), 'api_url' => $listenersUrl, ]; } } else { $results['listeners'] = [ 'status' => 'skipped', 'message' => 'Geen listeners API geconfigureerd', ]; } return $results; } public function detectAzureCast(): array { $baseUrl = $this->getSetting(RadioSettings::AzureCastBaseUrl); if (! empty($baseUrl)) { return ['detected' => true, 'base_url' => $baseUrl]; } $streamUrl = $this->getSetting(RadioSettings::StreamUrl, ''); if (empty($streamUrl)) { return ['detected' => false]; } $parsed = parse_url((string) $streamUrl); if (! $parsed) { return ['detected' => false]; } $scheme = $parsed['scheme'] ?? 'https'; $host = $parsed['host'] ?? ''; $testUrl = $scheme . '://' . $host . '/api/nowplaying'; try { $response = Http::timeout(3)->get($testUrl); if ($response->successful()) { $data = $response->json(); if (is_array($data) && (isset($data[0]['station']) || isset($data['station']))) { $stationId = $data[0]['station']['id'] ?? $data['station']['id'] ?? 1; $detectedBaseUrl = $scheme . '://' . $host; $this->autoConfigureAzureCast($detectedBaseUrl, $stationId); return ['detected' => true, 'base_url' => $detectedBaseUrl, 'station_id' => $stationId]; } } } catch (\Exception) { // Not AzureCast } return ['detected' => false]; } public function getAzureCastApiUrl(): ?string { $baseUrl = $this->getSetting(RadioSettings::AzureCastBaseUrl); $stationId = $this->getSetting(RadioSettings::AzureCastStationId, '1'); if (! empty($baseUrl)) { return rtrim((string) $baseUrl, '/') . '/api/nowplaying/' . $stationId; } $streamUrl = $this->getSetting(RadioSettings::StreamUrl); if (! $streamUrl) { return null; } $parsed = parse_url((string) $streamUrl); if (! $parsed) { return null; } $scheme = $parsed['scheme'] ?? 'https'; $host = $parsed['host'] ?? ''; return $scheme . '://' . $host . '/api/nowplaying/' . $stationId; } public function getNowPlayingFromShoutcast(string $baseUrl): ?array { try { $response = Http::timeout(3)->get(rtrim($baseUrl, '/') . '/stats?json=1'); if ($response->successful()) { $data = $response->json(); $song = $data['songtitle'] ?? null; $artist = null; if ($song && str_contains($song, ' - ')) { $parts = explode(' - ', $song, 2); $artist = trim($parts[0]); $song = trim($parts[1]); } return [ 'song' => $song, 'artist' => $artist, 'raw' => $data['songtitle'] ?? null, ]; } } catch (\Exception) { // Silent fail } return null; } public function getNowPlayingFromIcecast(string $baseUrl): ?array { try { $response = Http::timeout(3)->get(rtrim($baseUrl, '/') . '/status-json.xsl'); if ($response->successful()) { $data = $response->json(); $icestats = $data['icestats'] ?? []; $source = $icestats['source'] ?? []; if (is_array($source)) { $title = $source['title'] ?? $source[0]['title'] ?? null; $song = null; $artist = null; if ($title && str_contains($title, ' - ')) { $parts = explode(' - ', $title, 2); $artist = trim($parts[0]); $song = trim($parts[1]); } else { $song = $title; } return [ 'song' => $song, 'artist' => $artist, 'raw' => $title, ]; } } } catch (\Exception) { // Silent fail } return null; } private function getSetting(RadioSettings $setting, mixed $default = null): mixed { return Cache::remember("setting_{$setting->value}", 60, function () use ($setting, $default): mixed { $websiteSetting = WebsiteSetting::where('key', $setting->value)->first(); return $websiteSetting?->value ?? $default; }); } private function autoConfigureAzureCast(string $baseUrl, int $stationId): void { $settings = [ RadioSettings::AzureCastBaseUrl->value => $baseUrl, RadioSettings::AzureCastStationId->value => $stationId, RadioSettings::NowPlayingEnabled->value => '1', RadioSettings::ListenersEnabled->value => '1', RadioSettings::ShowCurrentDj->value => '1', RadioSettings::WidgetEnabled->value => '1', RadioSettings::WidgetShowGlobally->value => '1', ]; foreach ($settings as $key => $value) { WebsiteSetting::updateOrCreate( ['key' => $key], ['value' => $value, 'comment' => 'Auto-configured'], ); } Cache::forget('radio_settings_*'); } }