You've already forked Atomcms-edit
Add multi-step radio wizard with Shoutcast/Icecast/AzureCast support and multi-language translations
- New 5-step RadioWizardController with session-based wizard flow - Enhanced RadioStreamService with Shoutcast/Icecast/ AzureCast auto-detection - Connection test functionality for stream, now-playing, and listeners - Wizard views for all 5 steps with step indicator navigation - All 21 language files updated with wizard translation keys (NL/EN + placeholders) - Wizard link added to existing radio setup page - Routes registered under /admin/radio/wizard/*
This commit is contained in:
+414
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Models\RadioRank;
|
||||
use App\Services\Community\RadioStreamService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RadioWizardController extends Controller
|
||||
{
|
||||
private const SESSION_KEY = 'radio_wizard';
|
||||
|
||||
public function __construct(
|
||||
private readonly RadioStreamService $streamService,
|
||||
) {}
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
$data = session()->get(self::SESSION_KEY);
|
||||
if (! $data) {
|
||||
session()->forget(self::SESSION_KEY);
|
||||
}
|
||||
|
||||
return view('admin.radio.wizard.step-1', [
|
||||
'step' => 1,
|
||||
'selectedPlatform' => $data['platform'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function processStep1(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'platform' => ['required', 'string', 'in:shoutcast,icecast,azurecast,other'],
|
||||
]);
|
||||
|
||||
session()->put(self::SESSION_KEY . '.platform', $validated['platform']);
|
||||
|
||||
return redirect()->route('admin.radio.wizard.step', ['step' => 2]);
|
||||
}
|
||||
|
||||
public function step(int $step): View|RedirectResponse
|
||||
{
|
||||
$data = session()->get(self::SESSION_KEY);
|
||||
|
||||
if (! $data && $step > 1) {
|
||||
return redirect()->route('admin.radio.wizard');
|
||||
}
|
||||
|
||||
return match ($step) {
|
||||
2 => $this->step2($data),
|
||||
3 => $this->step3($data),
|
||||
4 => $this->step4($data),
|
||||
5 => $this->step5($data),
|
||||
default => redirect()->route('admin.radio.wizard'),
|
||||
};
|
||||
}
|
||||
|
||||
private function step2(array $data): View
|
||||
{
|
||||
$platform = $data['platform'] ?? 'other';
|
||||
$platformLabels = [
|
||||
'shoutcast' => 'SHOUTcast',
|
||||
'icecast' => 'Icecast',
|
||||
'azurecast' => 'AzureCast',
|
||||
'other' => 'Anders',
|
||||
];
|
||||
|
||||
return view('admin.radio.wizard.step-2', [
|
||||
'step' => 2,
|
||||
'platform' => $platform,
|
||||
'platformLabel' => $platformLabels[$platform] ?? 'Onbekend',
|
||||
'streamUrl' => session()->get(self::SESSION_KEY . '.stream_url', ''),
|
||||
'streamName' => session()->get(self::SESSION_KEY . '.stream_name', ''),
|
||||
'azurecastBaseUrl' => session()->get(self::SESSION_KEY . '.azurecast_base_url', ''),
|
||||
'azurecastStationId' => session()->get(self::SESSION_KEY . '.azurecast_station_id', '1'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function processStep2(Request $request): RedirectResponse
|
||||
{
|
||||
$platform = session()->get(self::SESSION_KEY . '.platform', 'other');
|
||||
|
||||
$rules = [
|
||||
'stream_url' => ['required', 'string', 'max:500'],
|
||||
'stream_name' => ['nullable', 'string', 'max:100'],
|
||||
];
|
||||
|
||||
if ($platform === 'azurecast') {
|
||||
$rules['azurecast_base_url'] = ['nullable', 'string', 'max:500'];
|
||||
$rules['azurecast_station_id'] = ['nullable', 'integer', 'min:1'];
|
||||
}
|
||||
|
||||
$validated = $request->validate($rules);
|
||||
|
||||
session()->put(self::SESSION_KEY . '.stream_url', $validated['stream_url']);
|
||||
session()->put(self::SESSION_KEY . '.stream_name', $validated['stream_name'] ?? 'Mijn Radio');
|
||||
|
||||
if ($platform === 'azurecast') {
|
||||
session()->put(self::SESSION_KEY . '.azurecast_base_url', $validated['azurecast_base_url'] ?? '');
|
||||
session()->put(self::SESSION_KEY . '.azurecast_station_id', $validated['azurecast_station_id'] ?? '1');
|
||||
}
|
||||
|
||||
return redirect()->route('admin.radio.wizard.step', ['step' => 3]);
|
||||
}
|
||||
|
||||
private function step3(array $data): View
|
||||
{
|
||||
$platform = $data['platform'] ?? 'other';
|
||||
$streamUrl = $data['stream_url'] ?? '';
|
||||
|
||||
$autoDetected = null;
|
||||
if (! empty($streamUrl)) {
|
||||
$autoDetected = $this->streamService->detectStreamType($streamUrl);
|
||||
}
|
||||
|
||||
return view('admin.radio.wizard.step-3', [
|
||||
'step' => 3,
|
||||
'platform' => $platform,
|
||||
'autoDetected' => $autoDetected,
|
||||
'nowPlayingApi' => session()->get(self::SESSION_KEY . '.now_playing_api', $autoDetected['now_playing_api'] ?? ''),
|
||||
'listenersApi' => session()->get(self::SESSION_KEY . '.listeners_api', $autoDetected['listeners_api'] ?? ''),
|
||||
'enableNowPlaying' => session()->get(self::SESSION_KEY . '.enable_now_playing', true),
|
||||
'enableListeners' => session()->get(self::SESSION_KEY . '.enable_listeners', true),
|
||||
'enableCurrentDj' => session()->get(self::SESSION_KEY . '.enable_current_dj', true),
|
||||
]);
|
||||
}
|
||||
|
||||
public function processStep3(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'now_playing_api' => ['nullable', 'string', 'max:500'],
|
||||
'listeners_api' => ['nullable', 'string', 'max:500'],
|
||||
'enable_now_playing' => ['nullable', 'boolean'],
|
||||
'enable_listeners' => ['nullable', 'boolean'],
|
||||
'enable_current_dj' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
session()->put(self::SESSION_KEY . '.now_playing_api', $validated['now_playing_api'] ?? '');
|
||||
session()->put(self::SESSION_KEY . '.listeners_api', $validated['listeners_api'] ?? '');
|
||||
session()->put(self::SESSION_KEY . '.enable_now_playing', (bool) ($validated['enable_now_playing'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_listeners', (bool) ($validated['enable_listeners'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_current_dj', (bool) ($validated['enable_current_dj'] ?? false));
|
||||
|
||||
return redirect()->route('admin.radio.wizard.step', ['step' => 4]);
|
||||
}
|
||||
|
||||
private function step4(array $data): View
|
||||
{
|
||||
return view('admin.radio.wizard.step-4', [
|
||||
'step' => 4,
|
||||
'enableShouts' => session()->get(self::SESSION_KEY . '.enable_shouts', true),
|
||||
'enableApplications' => session()->get(self::SESSION_KEY . '.enable_applications', true),
|
||||
'enableWidget' => session()->get(self::SESSION_KEY . '.enable_widget', true),
|
||||
'enableWidgetGlobally' => session()->get(self::SESSION_KEY . '.enable_widget_globally', true),
|
||||
'enablePoints' => session()->get(self::SESSION_KEY . '.enable_points', true),
|
||||
'enableRequests' => session()->get(self::SESSION_KEY . '.enable_requests', true),
|
||||
'enableContests' => session()->get(self::SESSION_KEY . '.enable_contests', true),
|
||||
'enableGiveaways' => session()->get(self::SESSION_KEY . '.enable_giveaways', false),
|
||||
'enableDiscord' => session()->get(self::SESSION_KEY . '.enable_discord', false),
|
||||
'discordWebhook' => session()->get(self::SESSION_KEY . '.discord_webhook', ''),
|
||||
'widgetPosition' => session()->get(self::SESSION_KEY . '.widget_position', 'bottom-right'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function processStep4(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'enable_shouts' => ['nullable', 'boolean'],
|
||||
'enable_applications' => ['nullable', 'boolean'],
|
||||
'enable_widget' => ['nullable', 'boolean'],
|
||||
'enable_widget_globally' => ['nullable', 'boolean'],
|
||||
'enable_points' => ['nullable', 'boolean'],
|
||||
'enable_requests' => ['nullable', 'boolean'],
|
||||
'enable_contests' => ['nullable', 'boolean'],
|
||||
'enable_giveaways' => ['nullable', 'boolean'],
|
||||
'enable_discord' => ['nullable', 'boolean'],
|
||||
'discord_webhook' => ['nullable', 'string', 'max:500'],
|
||||
'widget_position' => ['nullable', 'string', 'in:bottom-right,bottom-left,top-right,top-left'],
|
||||
]);
|
||||
|
||||
session()->put(self::SESSION_KEY . '.enable_shouts', (bool) ($validated['enable_shouts'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_applications', (bool) ($validated['enable_applications'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_widget', (bool) ($validated['enable_widget'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_widget_globally', (bool) ($validated['enable_widget_globally'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_points', (bool) ($validated['enable_points'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_requests', (bool) ($validated['enable_requests'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_contests', (bool) ($validated['enable_contests'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_giveaways', (bool) ($validated['enable_giveaways'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.enable_discord', (bool) ($validated['enable_discord'] ?? false));
|
||||
session()->put(self::SESSION_KEY . '.discord_webhook', $validated['discord_webhook'] ?? '');
|
||||
session()->put(self::SESSION_KEY . '.widget_position', $validated['widget_position'] ?? 'bottom-right');
|
||||
|
||||
return redirect()->route('admin.radio.wizard.step', ['step' => 5]);
|
||||
}
|
||||
|
||||
private function step5(array $data): View
|
||||
{
|
||||
$platform = $data['platform'] ?? 'other';
|
||||
$streamUrl = $data['stream_url'] ?? '';
|
||||
|
||||
$platformLabels = [
|
||||
'shoutcast' => 'SHOUTcast',
|
||||
'icecast' => 'Icecast',
|
||||
'azurecast' => 'AzureCast',
|
||||
'other' => 'Anders',
|
||||
];
|
||||
|
||||
$testResults = null;
|
||||
|
||||
if (! empty($streamUrl)) {
|
||||
$testResults = $this->streamService->testStreamConnection(
|
||||
$streamUrl,
|
||||
$data['now_playing_api'] ?? null,
|
||||
$data['listeners_api'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
$settingsList = $this->buildSettingsList($data);
|
||||
|
||||
return view('admin.radio.wizard.step-5', [
|
||||
'step' => 5,
|
||||
'platform' => $platform,
|
||||
'platformLabel' => $platformLabels[$platform] ?? 'Onbekend',
|
||||
'data' => $data,
|
||||
'testResults' => $testResults,
|
||||
'settingsList' => $settingsList,
|
||||
]);
|
||||
}
|
||||
|
||||
public function runTest(): JsonResponse
|
||||
{
|
||||
$data = session()->get(self::SESSION_KEY);
|
||||
|
||||
if (! $data || empty($data['stream_url'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Geen stream URL gevonden. Start de wizard opnieuw.',
|
||||
]);
|
||||
}
|
||||
|
||||
$testResults = $this->streamService->testStreamConnection(
|
||||
$data['stream_url'],
|
||||
$data['now_playing_api'] ?? null,
|
||||
$data['listeners_api'] ?? null,
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'results' => $testResults,
|
||||
]);
|
||||
}
|
||||
|
||||
public function complete(): RedirectResponse
|
||||
{
|
||||
$data = session()->get(self::SESSION_KEY);
|
||||
|
||||
if (! $data) {
|
||||
return redirect()->route('admin.radio.wizard')
|
||||
->with('error', 'Geen wizard data gevonden. Start opnieuw.');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->saveAllSettings($data);
|
||||
$this->createDefaultRanks();
|
||||
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('cache:clear');
|
||||
|
||||
session()->forget(self::SESSION_KEY);
|
||||
|
||||
return redirect()->route('admin.radio.setup')
|
||||
->with('success', 'Radio systeem is succesvol geïnstalleerd en geconfigureerd!');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('admin.radio.wizard.step', ['step' => 5])
|
||||
->with('error', 'Fout tijdens opslaan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function saveAllSettings(array $data): void
|
||||
{
|
||||
$platform = $data['platform'] ?? 'other';
|
||||
|
||||
$settings = [
|
||||
'radio_enabled' => '1',
|
||||
'radio_stream_url' => $data['stream_url'] ?? '',
|
||||
'radio_stream_name' => $data['stream_name'] ?? 'Mijn Radio',
|
||||
'radio_style' => 'dark',
|
||||
'radio_auto_play' => '0',
|
||||
'radio_stream_platform' => $platform,
|
||||
'radio_now_playing_enabled' => ($data['enable_now_playing'] ?? true) ? '1' : '0',
|
||||
'radio_listeners_enabled' => ($data['enable_listeners'] ?? true) ? '1' : '0',
|
||||
'radio_show_current_dj' => ($data['enable_current_dj'] ?? true) ? '1' : '0',
|
||||
'radio_shouts_enabled' => ($data['enable_shouts'] ?? true) ? '1' : '0',
|
||||
'radio_applications_enabled' => ($data['enable_applications'] ?? true) ? '1' : '0',
|
||||
'radio_widget_enabled' => ($data['enable_widget'] ?? true) ? '1' : '0',
|
||||
'radio_widget_show_globally' => ($data['enable_widget_globally'] ?? true) ? '1' : '0',
|
||||
'radio_widget_position' => $data['widget_position'] ?? 'bottom-right',
|
||||
'radio_auto_dj_detection' => '1',
|
||||
'radio_show_song_history' => '1',
|
||||
'radio_show_schedule_preview' => '1',
|
||||
'radio_word_filter_enabled' => '1',
|
||||
];
|
||||
|
||||
if ($platform === 'azurecast') {
|
||||
$settings['radio_azurecast_base_url'] = $data['azurecast_base_url'] ?? '';
|
||||
$settings['radio_azurecast_station_id'] = $data['azurecast_station_id'] ?? '1';
|
||||
}
|
||||
|
||||
if (! empty($data['now_playing_api'])) {
|
||||
$settings['radio_now_playing_api_url'] = $data['now_playing_api'];
|
||||
}
|
||||
|
||||
if (! empty($data['listeners_api'])) {
|
||||
$settings['radio_listeners_api_url'] = $data['listeners_api'];
|
||||
}
|
||||
|
||||
if ($data['enable_points'] ?? true) {
|
||||
$settings['points_enabled'] = '1';
|
||||
$settings['points_per_minute'] = '2';
|
||||
$settings['max_points_per_day'] = '100';
|
||||
$settings['points_for_request'] = '5';
|
||||
$settings['points_for_vote'] = '2';
|
||||
$settings['points_for_giveaway_win'] = '50';
|
||||
$settings['points_for_contest_win'] = '100';
|
||||
}
|
||||
|
||||
if ($data['enable_requests'] ?? true) {
|
||||
$settings['radio_request_form_enabled'] = '1';
|
||||
}
|
||||
|
||||
if ($data['enable_contests'] ?? true) {
|
||||
$settings['radio_contests_enabled'] = '1';
|
||||
}
|
||||
|
||||
if ($data['enable_giveaways'] ?? false) {
|
||||
$settings['radio_giveaways_enabled'] = '1';
|
||||
}
|
||||
|
||||
if ($data['enable_discord'] ?? false && ! empty($data['discord_webhook'])) {
|
||||
$settings['radio_discord_enabled'] = '1';
|
||||
$settings['radio_discord_webhook_url'] = $data['discord_webhook'];
|
||||
$settings['radio_discord_dj_live'] = '1';
|
||||
$settings['radio_discord_song_changes'] = '1';
|
||||
}
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => (string) $value, 'comment' => 'Radio wizard configuratie'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function createDefaultRanks(): void
|
||||
{
|
||||
$ranks = [
|
||||
['name' => 'Trainee DJ', 'level' => 1, 'is_active' => true, 'description' => 'Beginnende DJ'],
|
||||
['name' => 'Junior DJ', 'level' => 2, 'is_active' => true, 'description' => 'Ervaren DJ'],
|
||||
['name' => 'Senior DJ', 'level' => 3, 'is_active' => true, 'description' => 'Professionele DJ'],
|
||||
['name' => 'Head DJ', 'level' => 4, 'is_active' => true, 'description' => 'Hoofd DJ'],
|
||||
['name' => 'Radio Manager', 'level' => 5, 'is_active' => true, 'description' => 'Radio Manager'],
|
||||
];
|
||||
|
||||
foreach ($ranks as $rank) {
|
||||
RadioRank::updateOrCreate(
|
||||
['name' => $rank['name']],
|
||||
$rank,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildSettingsList(array $data): array
|
||||
{
|
||||
$list = [];
|
||||
$list['Stream URL'] = $data['stream_url'] ?? '-';
|
||||
$list['Stream Naam'] = $data['stream_name'] ?? 'Mijn Radio';
|
||||
$list['Platform'] = match ($data['platform'] ?? 'other') {
|
||||
'shoutcast' => 'SHOUTcast',
|
||||
'icecast' => 'Icecast',
|
||||
'azurecast' => 'AzureCast',
|
||||
default => 'Anders',
|
||||
};
|
||||
|
||||
if (! empty($data['now_playing_api'])) {
|
||||
$list['Now Playing API'] = $data['now_playing_api'];
|
||||
}
|
||||
|
||||
if (! empty($data['listeners_api'])) {
|
||||
$list['Listeners API'] = $data['listeners_api'];
|
||||
}
|
||||
|
||||
$list['Nu Afspelen'] = ($data['enable_now_playing'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Luisteraars'] = ($data['enable_listeners'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Huidige DJ'] = ($data['enable_current_dj'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Shouts'] = ($data['enable_shouts'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['DJ Aanmeldingen'] = ($data['enable_applications'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Radio Widget'] = ($data['enable_widget'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Widget Overal'] = ($data['enable_widget_globally'] ?? true) ? 'Ja' : 'Nee';
|
||||
$list['Widget Positie'] = $data['widget_position'] ?? 'bottom-right';
|
||||
$list['Punten Systeem'] = ($data['enable_points'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Song Verzoeken'] = ($data['enable_requests'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Contesten'] = ($data['enable_contests'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Giveaways'] = ($data['enable_giveaways'] ?? false) ? 'Aan' : 'Uit';
|
||||
$list['Discord'] = ($data['enable_discord'] ?? false) ? 'Aan' : 'Uit';
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,19 @@ class RadioStreamService
|
||||
}
|
||||
|
||||
try {
|
||||
return Http::timeout(2)
|
||||
$response = Http::timeout(2)
|
||||
->withOptions(['verify' => false])
|
||||
->head($streamUrl)
|
||||
->successful();
|
||||
->head($streamUrl);
|
||||
|
||||
if ($response->successful()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$response = Http::timeout(2)
|
||||
->withOptions(['verify' => false])
|
||||
->get($streamUrl);
|
||||
|
||||
return $response->successful();
|
||||
} catch (\Exception) {
|
||||
return false;
|
||||
}
|
||||
@@ -105,6 +114,355 @@ class RadioStreamService
|
||||
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);
|
||||
@@ -171,6 +529,70 @@ class RadioStreamService
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user