*/ private array $settingsCache = []; /** * Get multiple settings at once (batched query) * * @param array $keys * * @return array */ private function getSettings(array $keys): array { $cacheKey = 'radio_settings_' . md5(implode(',', $keys)); return Cache::remember($cacheKey, 60, function () use ($keys): array { /** @var Collection $collection */ $collection = WebsiteSetting::whereIn('key', $keys) ->pluck('value', 'key'); return $collection->all(); }); } /** * Get single setting with caching */ private function getSetting(string $key, mixed $default = null): mixed { if (! isset($this->settingsCache[$key])) { $this->settingsCache[$key] = Cache::remember("setting_{$key}", 60, function () use ($key): mixed { /** @var WebsiteSetting|null $setting */ $setting = WebsiteSetting::where('key', $key)->first(); return $setting?->value; }); } return $this->settingsCache[$key] ?? $default; } public function index(): View { // Batch load all radio settings at once $settings = $this->getSettings([ 'radio_enabled', 'radio_stream_url', 'radio_current_dj_id', ]); if (! (bool) ($settings['radio_enabled'] ?? false)) { return view('community.radio.disabled'); } // Load all data with single queries each $ranks = Cache::remember('radio_ranks', 300, fn () => RadioRank::all()); $banners = Cache::remember('radio_banners_active', 60, fn () => RadioBanner::with('user:id,username,look') ->active() ->ordered() ->take(10) ->get()); $todaySchedule = Cache::remember('radio_schedule_today', 60, fn () => RadioSchedule::with('user:id,username,look') ->active() ->today() ->orderBy('start_time') ->get()); $currentDJ = $this->getCurrentDJFromSchedule(); $streamUrl = $this->formatStreamUrl($settings['radio_stream_url'] ?? ''); // Cache stream status check (30 seconds) $isOnline = Cache::remember('radio_stream_status', 30, fn () => $this->checkStreamOnline($streamUrl)); return view('community.radio.index', ['ranks' => $ranks, 'banners' => $banners, 'todaySchedule' => $todaySchedule, 'currentDJ' => $currentDJ, 'isOnline' => $isOnline, 'streamUrl' => $streamUrl]); } public function rooster(): View { $schedule = Cache::remember('radio_schedule_all', 300, fn () => RadioSchedule::with('user:id,username,look') ->active() ->ordered() ->get() ->groupBy('day')); return view('community.radio.rooster', ['schedule' => $schedule]); } public function shouts(): RedirectResponse|View { if (! (bool) $this->getSetting('radio_shouts_enabled')) { return redirect()->route('radio.index')->with('error', __('radio.shouts_disabled')); } return view('community.radio.shouts'); } public function apply(): RedirectResponse|View { if (! (bool) $this->getSetting('radio_applications_enabled')) { return redirect()->route('radio.index')->with('error', __('radio.applications_closed')); } $userId = auth()->id(); $hasPendingApplication = $userId && RadioApplication::where('user_id', $userId) ->where('status', 'pending') ->exists(); $ranks = Cache::remember('radio_ranks_accepting', 300, fn () => RadioRank::where('accepts_applications', true)->get()); return view('community.radio.apply', ['ranks' => $ranks, 'hasPendingApplication' => $hasPendingApplication]); } public function storeApplication(Request $request): RedirectResponse { if (! (bool) $this->getSetting('radio_applications_enabled')) { return redirect()->route('radio.index')->with('error', __('radio.applications_closed')); } /** @var int|string $userId */ $userId = auth()->id(); $hasPendingApplication = RadioApplication::where('user_id', $userId) ->where('status', 'pending') ->exists(); if ($hasPendingApplication) { return back()->withErrors([ 'general' => __('radio.application_pending'), ])->withInput(); } $validated = $request->validate([ 'real_name' => ['required', 'string', 'max:255'], 'age' => ['required', 'integer', 'min:13', 'max:100'], 'availability' => ['required', 'string', 'min:10'], 'experience' => ['nullable', 'string'], 'motivation' => ['required', 'string', 'min:50'], 'music_style' => ['nullable', 'string', 'max:500'], 'discord_username' => ['nullable', 'string', 'max:255'], 'rank_id' => ['nullable', 'exists:radio_ranks,id'], ]); // Check rank accepts applications (cached) if ($validated['rank_id']) { $rank = Cache::remember("radio_rank_{$validated['rank_id']}", 300, fn () => RadioRank::find($validated['rank_id'])); if (! $rank || ! $rank->accepts_applications) { return back()->withErrors([ 'rank_id' => __('radio.rank_not_accepting', ['rank' => $rank?->name ?? 'Deze rank']), ])->withInput(); } } RadioApplication::create([ 'user_id' => $userId, 'real_name' => $validated['real_name'], 'age' => $validated['age'], 'availability' => $validated['availability'], 'experience' => $validated['experience'] ?? null, 'motivation' => $validated['motivation'], 'music_style' => $validated['music_style'] ?? null, 'discord_username' => $validated['discord_username'] ?? null, 'rank_id' => $validated['rank_id'] ?? null, 'status' => 'pending', ]); return redirect()->route('radio.apply')->with('success', __('radio.application_sent')); } public function storeShout(Request $request): RedirectResponse { if (! (bool) $this->getSetting('radio_shouts_enabled')) { return redirect()->route('radio.index')->with('error', __('radio.shouts_disabled')); } $validated = $request->validate([ 'message' => ['required', 'string', 'max:280'], ]); RadioShout::create([ 'user_id' => auth()->id(), 'message' => $validated['message'], ]); // Clear shouts cache Cache::forget('radio_shouts_recent'); return redirect()->route('radio.shouts')->with('success', __('radio.shout_sent')); } public function nowPlaying(): JsonResponse { // Cache now playing for 10 seconds to reduce API calls $nowPlaying = Cache::remember('radio_nowplaying', 10, fn () => $this->getNowPlaying()); return response()->json($nowPlaying); } public function listeners(): JsonResponse { // Cache listeners count for 30 seconds $count = Cache::remember('radio_listeners', 30, fn () => $this->getListenersCount()); return response()->json(['count' => $count]); } public function currentDJ(): JsonResponse { $dj = $this->getCurrentDJFromSchedule(); return response()->json([ 'dj' => $dj, 'is_live' => $dj !== null, ]); } private function getCurrentDJFromSchedule(): ?array { $manualDjId = $this->getSetting('radio_current_dj_id'); if (! empty($manualDjId)) { $dj = Cache::remember("user_{$manualDjId}", 60, fn () => User::find($manualDjId)); if ($dj) { return [ 'username' => $dj->username, 'look' => $dj->look, 'show_name' => 'Live DJ', 'is_manual' => true, ]; } } $currentSlot = RadioSchedule::with('user:id,username,look') ->active() ->where('day', $this->getCurrentDay()) ->whereTime('start_time', '<=', now()->format('H:i:s')) ->whereTime('end_time', '>=', now()->format('H:i:s')) ->first(); if ($currentSlot?->user) { return [ 'username' => $currentSlot->user->username, 'look' => $currentSlot->user->look, 'show_name' => $currentSlot->show_name, 'start_time' => $currentSlot->start_time->format('H:i'), 'end_time' => $currentSlot->end_time->format('H:i'), 'is_manual' => false, ]; } return null; } private function getNowPlaying(): array { if (! (bool) $this->getSetting('radio_now_playing_enabled')) { return ['enabled' => false, 'song' => null]; } $apiUrl = $this->getSetting('radio_now_playing_api_url') ?: $this->getAzureCastApiUrl(); 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]; } private function getAzureCastApiUrl(): ?string { $baseUrl = $this->getSetting('radio_azurecast_base_url'); $stationId = $this->getSetting('radio_azurecast_station_id', '1'); if (! empty($baseUrl)) { return rtrim((string) $baseUrl, '/') . '/api/nowplaying/' . $stationId; } $streamUrl = $this->getSetting('radio_stream_url'); 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; } private function getListenersCount(): int { if (! (bool) $this->getSetting('radio_listeners_enabled')) { return 0; } $apiUrl = $this->getSetting('radio_listeners_api_url') ?: $this->getAzureCastApiUrl(); 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; } private function getCurrentDay(): string { return strtolower(now()->format('l')); } private 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; } private function checkStreamOnline(string $streamUrl): bool { if ($streamUrl === '' || $streamUrl === '0') { return false; } try { return Http::timeout(2)->withOptions(['verify' => false])->head($streamUrl)->successful(); } catch (\Exception) { return false; } } private function detectAzureCast(): bool { if ($this->azureCastDetected !== null) { return $this->azureCastDetected; } $baseUrl = $this->getSetting('radio_azurecast_base_url'); if (! empty($baseUrl)) { $this->azureCastDetected = true; return true; } $streamUrl = $this->getSetting('radio_stream_url', ''); if (empty($streamUrl)) { $this->azureCastDetected = false; return false; } $parsed = parse_url((string) $streamUrl); if (! $parsed) { $this->azureCastDetected = false; return 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']))) { $this->azureCastDetected = true; $stationId = $data[0]['station']['id'] ?? $data['station']['id'] ?? 1; $detectedBaseUrl = $scheme . '://' . $host; WebsiteSetting::updateOrCreate( ['key' => 'radio_azurecast_base_url'], ['value' => $detectedBaseUrl, 'comment' => 'Auto-detected AzureCast'], ); WebsiteSetting::updateOrCreate( ['key' => 'radio_azurecast_station_id'], ['value' => $stationId, 'comment' => 'Auto-detected Station ID'], ); WebsiteSetting::updateOrCreate( ['key' => 'radio_now_playing_enabled'], ['value' => '1', 'comment' => 'Auto-enabled'], ); WebsiteSetting::updateOrCreate( ['key' => 'radio_listeners_enabled'], ['value' => '1', 'comment' => 'Auto-enabled'], ); WebsiteSetting::updateOrCreate( ['key' => 'radio_show_current_dj'], ['value' => '1', 'comment' => 'Auto-enabled'], ); WebsiteSetting::updateOrCreate( ['key' => 'radio_widget_enabled'], ['value' => '1', 'comment' => 'Auto-enabled'], ); WebsiteSetting::updateOrCreate( ['key' => 'radio_widget_show_globally'], ['value' => '1', 'comment' => 'Auto-enabled'], ); $this->settingsCache = []; return true; } } } catch (\Exception) { // Not AzureCast } $this->azureCastDetected = false; return false; } public function config(): JsonResponse { $settings = $this->getSettings([ 'radio_enabled', 'radio_stream_url', 'radio_style', 'radio_now_playing_enabled', 'radio_listeners_enabled', 'radio_show_current_dj', 'radio_widget_enabled', 'radio_widget_show_globally', 'radio_widget_position', ]); $streamUrl = $this->formatStreamUrl($settings['radio_stream_url'] ?? ''); $isAzurecast = $this->detectAzureCast(); return response()->json([ 'enabled' => (bool) ($settings['radio_enabled'] ?? false), 'stream_url' => $streamUrl, 'style' => $settings['radio_style'] ?? 'dark', 'dj' => $this->getCurrentDJFromSchedule(), 'now_playing_enabled' => (bool) ($settings['radio_now_playing_enabled'] ?? false), 'listeners_enabled' => (bool) ($settings['radio_listeners_enabled'] ?? false), 'show_current_dj' => (bool) ($settings['radio_show_current_dj'] ?? false), 'widget_enabled' => (bool) ($settings['radio_widget_enabled'] ?? false), 'widget_show_globally' => (bool) ($settings['radio_widget_show_globally'] ?? false), 'widget_position' => $settings['radio_widget_position'] ?? 'bottom-right', 'is_azurecast' => $isAzurecast, 'azurecast_detected' => $isAzurecast, ]); } public function startSession(Request $request): JsonResponse { $userId = auth()->id(); $activeSession = RadioHistory::where('user_id', $userId) ->whereNull('ended_at') ->first(); if ($activeSession) { return response()->json([ 'error' => 'Je hebt al een actieve sessie', 'session_id' => $activeSession->id, ], 400); } $session = RadioHistory::create([ 'user_id' => $userId, 'show_name' => $request->input('show_name'), 'started_at' => now(), ]); return response()->json([ 'message' => 'Sessie gestart', 'session_id' => $session->id, ]); } public function endSession(): JsonResponse { $userId = auth()->id(); $activeSession = RadioHistory::where('user_id', $userId) ->whereNull('ended_at') ->first(); if (! $activeSession) { return response()->json([ 'error' => 'Geen actieve sessie gevonden', ], 404); } $activeSession->endSession(); return response()->json([ 'message' => 'Sessie beƫindigd', 'duration' => $activeSession->duration, ]); } public function getShouts(): JsonResponse { if (! (bool) $this->getSetting('radio_shouts_enabled')) { return response()->json([ 'error' => 'Shouts zijn uitgeschakeld', 'shouts' => [], ], 403); } $shouts = Cache::remember('radio_shouts_recent', 30, fn () => RadioShout::with('user:id,username') ->orderBy('created_at', 'desc') ->take(50) ->get() ->map(fn ($shout) => [ 'id' => $shout->id, 'username' => $shout->user?->username ?? 'Anoniem', 'message' => $shout->message, 'created_at' => $shout->created_at->diffForHumans(), ])); return response()->json([ 'shouts' => $shouts, 'total' => $shouts->count(), ]); } }