Add broadcast scheduling features: implement Show and Channel entities, enhance AppDbContext and DependencyInjection for broadcasting, and update API routing. Include migration for new database schema and update documentation for broadcast-related functionalities.
This commit is contained in:
@@ -60,8 +60,9 @@
|
||||
- `Broadcast/Channel` — slug, название, вкл/выкл, `EpochUtc`, политика рекламы
|
||||
(`AdInsertion: BetweenBlocks | BetweenEpisodes`, `AdsPerBreak`), `FillerAssetId` (подстраховка).
|
||||
- `Broadcast/ChannelShow` — связка канал↔шоу: `Weight` (частота в случайном выборе), `BlockMode`
|
||||
(`Count` | `Duration`) и `BlockValue` (число серий K либо бюджет в минутах M), вкл/выкл. Для
|
||||
`Single` блок всегда = 1 серия. «Шоу по одной серии» = `Count`/K=1.
|
||||
(`Count` | `Duration`) и `BlockValue` (число серий K либо бюджет в минутах M), вкл/выкл, а также
|
||||
`NextEpisodeIndex` — **персональный для этого канала курсор серий**. Для `Single` блок = 1 серия.
|
||||
«Шоу по одной серии» = `Count`/K=1.
|
||||
- `Broadcast/ChannelAd` — связка канал↔рекламный ассет: пул врезок канала.
|
||||
- `Broadcast/ProgrammingOverride` — временный override на канале: окно `[StartsAtUtc, EndsAtUtc]`,
|
||||
режим (`Exclusive` — только это шоу; `Boost` — подменённые веса), ссылка(и) на шоу. На пересечении
|
||||
@@ -72,12 +73,15 @@
|
||||
`StartsAtUtc`, `EntryKind` (`Program | Ad`), для программ — `ShowId` и номер серии (для EPG).
|
||||
Генерируется планировщиком на несколько дней вперёд.
|
||||
|
||||
Курсоров как отдельного состояния **нет**. «Следующая серия шоу X на канале Y» — это чистая
|
||||
функция от расписания: берём последнюю запланированную серию этого шоу на этом канале, находим её
|
||||
позицию в упорядоченном списке серий, следующая — `(pos + 1) % count` (отсюда же цикл с начала).
|
||||
Первая генерация (записей ещё нет) стартует с первой серии. Рекламная ротация выводится так же —
|
||||
из последней врезки. Единственное персистентное состояние эфира — сами строки `ScheduleEntry`,
|
||||
рассинхронизироваться нечему.
|
||||
**Курсоры серий и рекламы — персистентные** (`ChannelShow.NextEpisodeIndex`, `Channel.NextAdIndex`),
|
||||
их двигает планировщик по мере постановки в расписание. Так серии идут по порядку и продолжаются
|
||||
через дни, а курсор переживает чистку старых `ScheduleEntry` по ретеншну (вывод «из хвоста» этого не
|
||||
гарантировал бы для редко играемых шоу). Заворот на первую серию — `(index + 1) % count`.
|
||||
|
||||
Компромисс: при **перегенерации** будущего хвоста (правка конфигурации канала) удалённые из будущего
|
||||
серии уже «отмотаны» курсором вперёд и однократно пропускаются — для домашнего канала это незаметно,
|
||||
зато код без сложной «перемотки» курсоров. Расширение горизонта (обычный тик) ничего не удаляет и
|
||||
пропусков не даёт.
|
||||
|
||||
## Планировщик
|
||||
|
||||
@@ -93,7 +97,7 @@
|
||||
2. **Взвешенно-случайный выбор** включённого `ChannelShow` по действующим весам (`System.Random`,
|
||||
seed на генерацию логируется для воспроизводимости; отдельного состояния не требует, т.к.
|
||||
результат материализуется).
|
||||
3. Набрать блок серий подряд от «следующей» (см. вывод из хвоста выше), с заворотом на первую по
|
||||
3. Набрать блок серий подряд от «следующей» (курсор `NextEpisodeIndex`), с заворотом на первую по
|
||||
достижении конца:
|
||||
- `BlockMode = Count` — ровно `BlockValue` серий;
|
||||
- `BlockMode = Duration` — добавлять серии, пока суммарная длительность не достигнет `BlockValue`
|
||||
@@ -109,12 +113,11 @@
|
||||
предыдущей. Так как каждая длительность кратна 2с (см. ниже), все старты автоматически кратны 2с
|
||||
от эпохи — выравнивание на сегмент держится само собой, без часовой сетки и без дыр.
|
||||
|
||||
**Правка на лету.** Меняются веса/блоки/реклама, состав шоу или добавляется/снимается override →
|
||||
перегенерируется только будущий хвост от безопасной точки (конец текущей программы + буфер), прошлое
|
||||
не трогаем. Поскольку «курсор» выводится из хвоста, обрезав будущие записи, планировщик просто
|
||||
продолжит от последней оставшейся — двойного проигрывания серий не будет. Override, начинающийся
|
||||
позже, подхватится штатным тиком; override «начиная прямо сейчас» инициирует немедленную
|
||||
перегенерацию хвоста.
|
||||
**Правка на лету.** После изменения весов/блоков/рекламы/состава шоу или добавления override админ
|
||||
дёргает перегенерацию канала (`POST /api/admin/channels/{id}/regenerate`): удаляется ещё не
|
||||
стартовавший хвост (записи с `StartsAtUtc >= now`), сохраняется текущая идущая программа, и хвост
|
||||
достраивается заново от её конца. Обычный фоновый тик только **расширяет** горизонт (без удаления).
|
||||
Override, начинающийся в будущем, подхватится и штатным тиком.
|
||||
|
||||
**Ретеншн.** Прошедшие `ScheduleEntry` старше `Scheduler__RetentionHours` можно удалять (оставляя
|
||||
небольшое окно назад для EPG «что только что было»). Сегменты ассетов при этом не трогаются —
|
||||
|
||||
Reference in New Issue
Block a user