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:
root
2026-05-24 14:07:32 +02:00
parent 5476dce882
commit 0c6c558a59
32 changed files with 2236 additions and 29 deletions
+68
View File
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @method static \Illuminate\Database\Eloquent\Builder<static>|RadioAutoDjTrack where($column, $operator = null, $value = null)
* @method static \Illuminate\Database\Eloquent\Builder<static>|RadioAutoDjTrack create($attributes = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|RadioAutoDjTrack active()
* @method static \Illuminate\Database\Eloquent\Builder<static>|RadioAutoDjTrack orderBy($column, $direction = 'asc')
*
* @property string $title
* @property string|null $artist
* @property string|null $album
* @property string|null $artwork_url
* @property int|null $duration
* @property int $play_count
* @property \Carbon\Carbon|null $last_played_at
* @property bool $is_active
* @property int $sort_order
*/
class RadioAutoDjTrack extends Model
{
#[\Override]
protected $table = 'radio_auto_dj_playlist';
#[\Override]
protected $guarded = ['id', 'created_at', 'updated_at'];
#[\Override]
protected $casts = [
'duration' => 'integer',
'play_count' => 'integer',
'last_played_at' => 'datetime',
'is_active' => 'boolean',
'sort_order' => 'integer',
];
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeOrdered($query)
{
return $query->orderBy('sort_order')->orderBy('title');
}
public function markPlayed(): void
{
$this->increment('play_count');
$this->update(['last_played_at' => now()]);
}
public static function getNextTrack(): ?self
{
$last = self::active()->orderBy('last_played_at', 'asc')->orderBy('sort_order')->first();
if ($last) {
return $last;
}
return self::active()->ordered()->first();
}
}