Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
da87997
feat: Add FPS and bitrate to merge channel scoring (#330)
Warbs816 Apr 25, 2026
7421ce7
fix: Sample packets to measure video bitrate for live MPEG-TS streams
Warbs816 Apr 26, 2026
1a25601
feat: Periodic failover rescoring + virtual primary bulk action (#330)
Warbs816 Apr 26, 2026
e0e3d2a
feat: Surface scoring rationale for failover ranking
Warbs816 Apr 26, 2026
4e784d5
refactor: Normalize merge score to 0-100 instead of hundreds of thous…
Warbs816 Apr 26, 2026
8b01332
feat: Show failover ranking in channel info pane with rescore button
Warbs816 Apr 26, 2026
4f4b0c7
feat: Hide tech details for virtual primaries; expandable failover cards
Warbs816 Apr 26, 2026
4ecf077
refactor: Promote failover rescoring to its own playlist Section
Warbs816 Apr 26, 2026
1d08e84
refactor: Rename "virtual primary" to "smart channel" throughout
Warbs816 Apr 26, 2026
85a304b
feat: First-class is_smart_channel flag on channels
Warbs816 Apr 26, 2026
2556f13
refactor: Drop redundant helper text under smart-channel URL field
Warbs816 Apr 26, 2026
06d9331
feat: Smart Channel badge on Stream Monitor; fix list-column rendering
Warbs816 Apr 26, 2026
78d1bde
feat: Warning banner when smart channel has no failovers; sync i18n
Warbs816 Apr 26, 2026
e9e5979
fix: Cap probe sample timeout, eager-load rescore, guard smart-channe…
Warbs816 Apr 26, 2026
38782e1
fix: WithoutOverlapping middleware on RescoreChannelFailovers job
Warbs816 Apr 26, 2026
5faf95c
chore: Translate new smart channel strings into DE/FR/ES/zh_CN
Warbs816 Apr 27, 2026
972dcc9
Merge remote-tracking branch 'origin/dev' into feature/issue-330-smar…
Warbs816 Apr 30, 2026
b3c3fe7
Merge remote-tracking branch 'origin/dev' into feature/issue-330-smar…
Warbs816 May 2, 2026
c75510f
Merge remote-tracking branch 'origin/dev' into feature/issue-330-smar…
Warbs816 May 2, 2026
4144ae5
Merge branch 'dev' into feature/issue-330-smart-channels
Warbs816 May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions app/Console/Commands/RescoreChannelFailoversCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace App\Console\Commands;

use App\Jobs\RescoreChannelFailovers;
use App\Models\Playlist;
use Illuminate\Console\Command;

class RescoreChannelFailoversCommand extends Command
{
/**
* Allowed interval keys → number of seconds between rescores.
*/
private const INTERVALS = [
'daily' => 86400,
'weekly' => 604800,
];

protected $signature = 'app:rescore-channel-failovers {playlist? : Optional playlist ID to rescore directly}';

protected $description = 'Dispatch RescoreChannelFailovers for any playlist whose configured interval has elapsed';

public function handle(): int
{
$playlistId = $this->argument('playlist');

if ($playlistId !== null) {
$playlist = Playlist::find($playlistId);
if (! $playlist) {
$this->error("Playlist {$playlistId} not found");

return Command::FAILURE;
}

dispatch(new RescoreChannelFailovers($playlist->id));
$this->info("Dispatched failover rescore for playlist {$playlist->id}");

return Command::SUCCESS;
}

$playlists = Playlist::query()
->whereNotNull('auto_rescore_failovers_interval')
->get();

$dispatched = 0;
foreach ($playlists as $playlist) {
$intervalKey = strtolower((string) $playlist->auto_rescore_failovers_interval);
$intervalSeconds = self::INTERVALS[$intervalKey] ?? null;
if ($intervalSeconds === null) {
continue;
}

$lastRun = $playlist->last_failover_rescore_at;
if ($lastRun !== null && $lastRun->copy()->addSeconds($intervalSeconds)->gt(now())) {
continue;
}

dispatch(new RescoreChannelFailovers($playlist->id));
$dispatched++;
}

$this->info("Dispatched {$dispatched} playlist(s) for failover rescoring");

return Command::SUCCESS;
}
}
3 changes: 3 additions & 0 deletions app/Filament/Pages/M3uProxyStreamMonitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,13 @@ protected function getActiveStreams(): array
if (isset($stream['metadata']['type']) && isset($stream['metadata']['id'])) {
$modelType = $stream['metadata']['type'];
$modelId = $stream['metadata']['id'];
$isSmartChannel = false;
if ($modelType === 'channel') {
$channel = $channelsById[$modelId] ?? null;
if ($channel) {
$title = $channel->name_custom ?? $channel->name ?? $channel->title;
$logo = LogoFacade::getChannelLogoUrl($channel);
$isSmartChannel = (bool) $channel->is_smart_channel;
}
} elseif ($modelType === 'episode') {
$episode = $episodesById[$modelId] ?? null;
Expand All @@ -355,6 +357,7 @@ protected function getActiveStreams(): array
$model = [
'title' => $title ?? 'N/A',
'logo' => $logo,
'is_smart_channel' => $isSmartChannel,
];
}

Expand Down
326 changes: 321 additions & 5 deletions app/Filament/Resources/Channels/ChannelResource.php

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions app/Filament/Resources/Playlists/PlaylistResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use App\Jobs\ProcessM3uImport;
use App\Jobs\ProcessM3uImportSeries;
use App\Jobs\ProcessVodChannels;
use App\Jobs\RescoreChannelFailovers;
use App\Jobs\SyncMediaServer;
use App\Livewire\EpgViewer;
use App\Livewire\MediaFlowProxyUrl;
Expand Down Expand Up @@ -615,6 +616,26 @@ public static function getHeaderActions(): array
->send();
})
->visible(fn ($record) => $record->isProcessing()),
Action::make('rescore_failovers')
->label(__('Re-score failovers now'))
->icon('heroicon-o-arrow-trending-up')
->color('info')
->action(function ($record) {
app('Illuminate\Contracts\Bus\Dispatcher')
->dispatch(new RescoreChannelFailovers($record->id));
})->after(function () {
Notification::make()
->success()
->title(__('Failover rescoring queued'))
->body(__('Failover groups will be re-scored in the background. You can keep using the app while this runs.'))
->duration(5000)
->send();
})
->visible(fn ($record): bool => (bool) ($record->auto_merge_channels_enabled ?? false))
->requiresConfirmation()
->modalIcon('heroicon-o-arrow-trending-up')
->modalDescription(__('Queue a one-off failover rescore for this playlist? Stale channels will be re-probed (subject to the configured staleness window) and failovers re-sorted so the highest-quality source sits first.'))
->modalSubmitActionLabel(__('Yes, rescore now')),
Action::make('Download M3U')
->label(__('Download M3U'))
->icon('heroicon-o-arrow-down-tray')
Expand Down Expand Up @@ -2075,6 +2096,8 @@ public static function getFormSections($creating = false, $includeAuth = false):
'group_priority' => '📁 Group Priority (from weights above)',
'catchup_support' => '⏪ Catch-up/Replay Support',
'resolution' => '📺 Resolution (requires stream analysis)',
'fps' => '🎞️ Frame Rate (requires stream analysis)',
'bitrate' => '📊 Bitrate (requires stream analysis)',
'codec' => '🎬 Codec Preference (HEVC/H264)',
'keyword_match' => '🏷️ Keyword Match',
])
Expand All @@ -2100,6 +2123,30 @@ public static function getFormSections($creating = false, $includeAuth = false):
}
}),
]),

]),
Section::make(__('Failover Rescoring'))
->description(__('Keeps failover ordering in sync with current stream quality. Affects three places: scheduled rescoring (the interval below), the per-channel "Rescore now" action, and the "Make smart channel" bulk action. The Priority Order under Auto-Merge above is reused for the actual scoring in all three.'))
->columnSpanFull()
->collapsible()
->collapsed($creating)
->columns(2)
->schema([
Select::make('auto_rescore_failovers_interval')
->label(__('Periodic rescoring'))
->options([
'daily' => __('Daily'),
'weekly' => __('Weekly'),
])
->placeholder(__('Off'))
->helperText(__('Schedule a recurring rescore for every failover group in this playlist. Master channels are never promoted or replaced — only failover sort order changes. Off = rescore manually only via the "Rescore now" button.')),
TextInput::make('failover_rescore_staleness_days')
->label(__('Re-probe channels older than (days)'))
->numeric()
->default(7)
->minValue(0)
->maxValue(365)
->helperText(__('During scheduled or manual rescoring, channels with stats older than this are re-probed first. Set to 0 to always re-probe. The "Make smart channel" action uses existing stats only and does not consult this setting.')),
]),
Section::make(__('Find & Replace Rules'))
->description(__('Define find & replace rules that automatically run after each playlist sync. Rules execute in order.'))
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/PlaylistController.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ public function mergeChannels(Request $request, string $uuid): JsonResponse
],
'group_priorities.*.weight' => 'required_with:group_priorities|integer|min:1|max:1000',
'priority_attributes' => 'sometimes|array',
'priority_attributes.*' => 'string|in:playlist_priority,group_priority,catchup_support,resolution,codec,keyword_match',
'priority_attributes.*' => 'string|in:playlist_priority,group_priority,catchup_support,resolution,fps,bitrate,codec,keyword_match',
]);

$config = $playlist->auto_merge_config ?? [];
Expand Down
Loading
Loading