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,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RadioApiKey extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'key',
|
||||
'allowed_ips',
|
||||
'permissions',
|
||||
'rate_limit',
|
||||
'expires_at',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'permissions' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'last_used_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public static function generate(string $name, array $options = []): self
|
||||
{
|
||||
return static::create([
|
||||
'name' => $name,
|
||||
'key' => 'rad_' . Str::random(48),
|
||||
'allowed_ips' => $options['allowed_ips'] ?? null,
|
||||
'permissions' => $options['permissions'] ?? ['*'],
|
||||
'rate_limit' => $options['rate_limit'] ?? 300,
|
||||
'expires_at' => $options['expires_at'] ?? null,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
});
|
||||
}
|
||||
|
||||
public function hasPermission(string $permission): bool
|
||||
{
|
||||
$permissions = $this->permissions ?? ['*'];
|
||||
|
||||
return in_array('*', $permissions, true) || in_array($permission, $permissions, true);
|
||||
}
|
||||
|
||||
public function isAllowedIp(?string $ip): bool
|
||||
{
|
||||
if (empty($this->allowed_ips)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$allowed = array_map('trim', explode(',', $this->allowed_ips));
|
||||
|
||||
return in_array($ip, $allowed, true);
|
||||
}
|
||||
|
||||
public function touchLastUsed(): void
|
||||
{
|
||||
$this->update(['last_used_at' => now()]);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Services\Community\RadioScheduleService;
|
||||
use App\Services\Community\RadioStreamService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RadioSongPlay where($column, $operator = null, $value = null)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RadioSongPlay create($attributes = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RadioSongPlay orderBy($column, $direction = 'asc')
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RadioSongPlay latest($column = 'played_at')
|
||||
*
|
||||
* @property string $title
|
||||
* @property string|null $artist
|
||||
* @property string|null $album
|
||||
* @property string|null $artwork_url
|
||||
* @property int|null $duration
|
||||
* @property \Carbon\Carbon $played_at
|
||||
* @property int|null $dj_id
|
||||
* @property array|null $metadata
|
||||
*/
|
||||
class RadioSongPlay extends Model
|
||||
{
|
||||
#[\Override]
|
||||
protected $table = 'radio_song_plays';
|
||||
|
||||
#[\Override]
|
||||
protected $guarded = ['id', 'created_at', 'updated_at'];
|
||||
|
||||
#[\Override]
|
||||
protected $casts = [
|
||||
'played_at' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
'duration' => 'integer',
|
||||
];
|
||||
|
||||
public static function recordNowPlaying(?string $title, ?string $artist = null, ?array $extra = null): ?self
|
||||
{
|
||||
if (! $title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$last = self::latest('played_at')->first();
|
||||
|
||||
if ($last && $last->title === $title && $last->artist === $artist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$djId = WebsiteSetting::where('key', 'radio_current_dj_id')->first()?->value;
|
||||
|
||||
return self::create([
|
||||
'title' => $title,
|
||||
'artist' => $artist,
|
||||
'album' => $extra['album'] ?? null,
|
||||
'artwork_url' => $extra['artwork_url'] ?? null,
|
||||
'duration' => $extra['duration'] ?? null,
|
||||
'played_at' => $extra['played_at'] ?? now(),
|
||||
'dj_id' => $djId ? (int) $djId : null,
|
||||
'metadata' => $extra['metadata'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user