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:
Leonid Pershin
2026-07-24 08:57:08 +03:00
parent e15ecbdb29
commit 4fa9dae37f
86 changed files with 4094 additions and 15 deletions
+18 -15
View File
@@ -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 «что только что было»). Сегменты ассетов при этом не трогаются —