You've already forked Atomcms-edit
Add radio embed widget, SSE real-time, song history, moderation panel, and Auto DJ
- Embed widget: standalone iframe player with dark/light/transparent themes, copy-paste embed code admin page - Real-time SSE: streaming now-playing/listeners/dj events, replaces polling in radio-player and embed - Song history: auto-records song changes to radio_song_plays table, Filament resource to view - DJ moderation: unified panel for shouts approval, song request queue, DJ applications - Auto DJ: playlist management with round-robin playback when no DJ is live - Refactored radio-player Alpine component to use EventSource API with auto-reconnect
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Radio;
|
||||
|
||||
use App\Enums\RadioSettings;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Services\Community\RadioScheduleService;
|
||||
use App\Services\Community\RadioStreamService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class SseController extends Controller
|
||||
{
|
||||
private const int SSE_KEEPALIVE = 15;
|
||||
|
||||
public function __construct(
|
||||
private readonly RadioStreamService $streamService,
|
||||
private readonly RadioScheduleService $scheduleService,
|
||||
) {}
|
||||
|
||||
public function stream(Request $request): StreamedResponse
|
||||
{
|
||||
$channels = $request->query('channels', 'now-playing,listeners,dj');
|
||||
|
||||
$wanted = array_flip(array_map('trim', explode(',', $channels)));
|
||||
|
||||
$response = new StreamedResponse(function () use ($wanted): void {
|
||||
$last = [
|
||||
'now-playing' => null,
|
||||
'listeners' => null,
|
||||
'dj' => null,
|
||||
];
|
||||
|
||||
$this->sendSseComment('connected');
|
||||
|
||||
while (! connection_aborted()) {
|
||||
$sent = false;
|
||||
|
||||
if (isset($wanted['now-playing'])) {
|
||||
$data = $this->getNowPlaying();
|
||||
if ($data !== $last['now-playing']) {
|
||||
$this->sendSseEvent('now-playing', $data);
|
||||
$last['now-playing'] = $data;
|
||||
$sent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($wanted['listeners'])) {
|
||||
$count = $this->getListeners();
|
||||
if ($count !== $last['listeners']) {
|
||||
$this->sendSseEvent('listeners', ['count' => $count]);
|
||||
$last['listeners'] = $count;
|
||||
$sent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($wanted['dj'])) {
|
||||
$dj = $this->getCurrentDj();
|
||||
if ($dj !== $last['dj']) {
|
||||
$this->sendSseEvent('dj', $dj);
|
||||
$last['dj'] = $dj;
|
||||
$sent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $sent) {
|
||||
$this->sendSseComment('keepalive');
|
||||
}
|
||||
|
||||
ob_flush();
|
||||
flush();
|
||||
|
||||
sleep(5);
|
||||
}
|
||||
});
|
||||
|
||||
$response->headers->set('Content-Type', 'text/event-stream');
|
||||
$response->headers->set('Cache-Control', 'no-cache');
|
||||
$response->headers->set('Connection', 'keep-alive');
|
||||
$response->headers->set('X-Accel-Buffering', 'no');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function sendSseEvent(string $event, mixed $data): void
|
||||
{
|
||||
echo "event: {$event}\n";
|
||||
echo 'data: ' . json_encode($data) . "\n\n";
|
||||
}
|
||||
|
||||
private function sendSseComment(string $comment): void
|
||||
{
|
||||
echo ": {$comment}\n\n";
|
||||
}
|
||||
|
||||
private function getNowPlaying(): ?array
|
||||
{
|
||||
$autoDj = Cache::get('radio_auto_dj_active');
|
||||
|
||||
if ($autoDj !== null) {
|
||||
return [
|
||||
'enabled' => true,
|
||||
'song' => $autoDj['title'],
|
||||
'artist' => $autoDj['artist'] ?? null,
|
||||
'title' => $autoDj['title'],
|
||||
'is_auto_dj' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$cached = Cache::get('radio_nowplaying');
|
||||
|
||||
if ($cached !== null) {
|
||||
$cached['is_auto_dj'] = false;
|
||||
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$apiUrl = $this->getSetting(RadioSettings::NowPlayingEnabled)
|
||||
? ($this->getSetting(RadioSettings::NowPlayingApiUrl) ?: $this->getAzureCastApiUrl())
|
||||
: null;
|
||||
|
||||
$result = $apiUrl ? $this->streamService->getNowPlaying($apiUrl) : ['enabled' => false, 'song' => null];
|
||||
|
||||
$result['is_auto_dj'] = false;
|
||||
|
||||
Cache::put('radio_nowplaying', $result, 10);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getListeners(): int
|
||||
{
|
||||
return Cache::remember('radio_listeners', 30, function () {
|
||||
$apiUrl = $this->getSetting(RadioSettings::ListenersEnabled)
|
||||
? ($this->getSetting(RadioSettings::ListenersApiUrl) ?: $this->getAzureCastApiUrl())
|
||||
: null;
|
||||
|
||||
return $apiUrl ? $this->streamService->getListenersCount($apiUrl) : 0;
|
||||
});
|
||||
}
|
||||
|
||||
private function getCurrentDj(): ?array
|
||||
{
|
||||
$autoDj = Cache::get('radio_auto_dj_active');
|
||||
|
||||
$dj = $this->scheduleService->getCurrentDJ(
|
||||
$this->getSetting(RadioSettings::CurrentDjId)
|
||||
);
|
||||
|
||||
if ($dj === null && $autoDj !== null) {
|
||||
return [
|
||||
'username' => 'Auto DJ',
|
||||
'look' => null,
|
||||
'show_name' => 'Auto DJ',
|
||||
'is_auto_dj' => true,
|
||||
'song' => $autoDj['title'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
if ($dj !== null) {
|
||||
$dj['is_auto_dj'] = false;
|
||||
}
|
||||
|
||||
return $dj;
|
||||
}
|
||||
|
||||
private function getAzureCastApiUrl(): ?string
|
||||
{
|
||||
$baseUrl = $this->getSetting(RadioSettings::AzureCastBaseUrl);
|
||||
$stationId = (int) $this->getSetting(RadioSettings::AzureCastStationId, '1');
|
||||
|
||||
if (! $baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rtrim($baseUrl, '/') . '/api/nowplaying/' . $stationId;
|
||||
}
|
||||
|
||||
private function getSetting(RadioSettings|string $key, mixed $default = null): mixed
|
||||
{
|
||||
$keyStr = $key instanceof RadioSettings ? $key->value : $key;
|
||||
|
||||
return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed {
|
||||
$setting = WebsiteSetting::where('key', $keyStr)->first();
|
||||
|
||||
return $setting?->value ?? $default;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user