412 lines
23 KiB
Markdown
412 lines
23 KiB
Markdown
# Планировщик эфира: задачи
|
||
|
||
Разбивка [tv-scheduler-architecture.md](tv-scheduler-architecture.md) на задачи для разработки.
|
||
Ссылки вида «см. 3.4» — на разделы спеки.
|
||
|
||
Задачи внутри среза идут в порядке зависимостей: следующая опирается на предыдущие. Срезы —
|
||
последовательно, каждый заканчивается работающим каналом.
|
||
|
||
---
|
||
|
||
## Общие требования к любой задаче
|
||
|
||
Выполняется для каждой задачи, ниже в тексте не повторяется:
|
||
|
||
- **Границы слоёв.** `Api → Infrastructure → Application → Domain`. В `Application` — только порты,
|
||
никаких `Npgsql`/`AspNetCore.Identity`.
|
||
- **Миграция.** Изменение модели → `dotnet ef migrations add <Name> --project src/TeleWave.Infrastructure --startup-project src/TeleWave.Api`.
|
||
- **Ошибки** — через `Result<T>` и каталоги `XErrors`, не исключениями. Валидация — FluentValidation.
|
||
- **Тесты.** Доменная логика — `tests/TeleWave.Domain.Tests`, хендлеры — `tests/TeleWave.Application.Tests`,
|
||
эндпоинты — `tests/TeleWave.Integration.Tests`. Сборка не должна давать предупреждений
|
||
(`TreatWarningsAsErrors=true`).
|
||
- **Фронт.** Типы и клиент — в `api.ts` соответствующей фичи. Все строки — в `frontend/src/shared/lib/i18n.ts`,
|
||
**обе локали** (`ru` и `en`). Проверка: `pnpm lint && pnpm typecheck`.
|
||
- **Изображения** — только через реестр `Image` и контрол `ImageGallery`, своих файловых полей не заводить.
|
||
|
||
---
|
||
|
||
## Срез 1 — сетка вещает
|
||
|
||
Цель: канал вещает по слотам вместо взвешенной ротации. Реклама и заставки временно работают как
|
||
сейчас, стыков ещё нет.
|
||
|
||
### 1A. Библиотека
|
||
|
||
#### 1.1. Справочник жанров
|
||
`Domain` `Infrastructure` `Application` `Api` `Frontend`
|
||
|
||
Сущность `Genre` (id, name, externalIds) и связь `ShowGenre` (showId, genreId, isPrimary).
|
||
Идемпотентный сид жанров TMDb в `DbInitializer`. CRUD жанров в админке (список, создание,
|
||
переименование, удаление с проверкой на использование).
|
||
|
||
Готово: жанры есть в БД после старта, админ может править справочник.
|
||
|
||
#### 1.2. Жанры на шоу
|
||
`Application` `Api` `Frontend`
|
||
|
||
Проставление жанров шоу вручную (мультиселект + выбор основного). Автоматический маппинг жанров
|
||
провайдера при `ApplyShowMetadata` / `UpdateShowMetadata` по `Genre.externalIds`; нераспознанные
|
||
жанры логируются, шоу не ломают. Фильтр по жанру в `ListShows`.
|
||
|
||
Готово: у шоу видны и правятся жанры, TMDb-метаданные проставляют их сами, список шоу фильтруется.
|
||
|
||
#### 1.3. Расширение `ShowAudience`
|
||
`Domain` `Infrastructure` `Frontend`
|
||
|
||
Пять значений по возрастанию строгости: `Kids → Family → Teen → General → Adult` (см. 3.1).
|
||
Текущий enum не отсортирован и переписывается; миграция переносит существующие значения
|
||
один-в-один. Обновить селекты и отображение в UI шоу.
|
||
|
||
Готово: порядок значений в enum монотонен по строгости, старые данные не потеряны.
|
||
|
||
#### 1.4. Коллекции
|
||
`Domain` `Infrastructure` `Application` `Api` `Frontend`
|
||
|
||
`Collection` (id, name, posterImageId) + `CollectionItem` (collectionId, showId, position).
|
||
CRUD, изменение порядка перетаскиванием, постер через `ImageGallery`. На экране шоу — блок
|
||
«входит в коллекции».
|
||
|
||
Готово: франшизу из трёх фильмов можно собрать, переупорядочить и открыть с экрана шоу.
|
||
|
||
### 1B. Группы
|
||
|
||
#### 1.5. Модель группы
|
||
`Domain` `Infrastructure`
|
||
|
||
`Group` (id, name, description, filter jsonb, cachedStats jsonb) + `GroupItem` (groupId,
|
||
elementKind, elementId, weight, position). См. 3.3. Группы глобальные, без привязки к каналу.
|
||
|
||
Готово: модель и миграция.
|
||
|
||
#### 1.6. CRUD групп и состава
|
||
`Application` `Api`
|
||
|
||
Создание/переименование/удаление группы (с проверкой, что она не используется слотами), добавление
|
||
и удаление позиций, изменение порядка и весов. Удаление шоу или коллекции из библиотеки чистит
|
||
позиции групп.
|
||
|
||
Готово: состав группы правится через API, висячих ссылок не остаётся.
|
||
|
||
#### 1.7. Фильтр набора и статистика
|
||
`Application` `Api`
|
||
|
||
Выполнение `filter` (см. 3.3) как **разового набора**: возвращает подходящие позиции, отдельная
|
||
команда добавляет их в состав. Пересчёт `cachedStats` (число позиций, число единиц, суммарная
|
||
длительность) при изменении состава и фоново при изменении библиотеки.
|
||
|
||
Готово: «добавить найденное по фильтру» работает, в API группы отдаётся актуальная статистика.
|
||
|
||
#### 1.8. Редактор группы
|
||
`Frontend`
|
||
|
||
Двухпанельный экран (см. 6.2): слева конструктор фильтра, справа состав с drag & drop порядка.
|
||
Перетаскивание шоу и коллекций из библиотеки. Вес — колонка за «дополнительно». Бейдж «доступно
|
||
N новых позиций по фильтру».
|
||
|
||
Готово: группу можно собрать целиком мышкой, статистика видна на экране.
|
||
|
||
### 1C. Шаблон, слои, слоты
|
||
|
||
#### 1.9. Модель шаблона
|
||
`Domain` `Infrastructure`
|
||
|
||
`ScheduleTemplate` (channelId, name, defaultJunctionId, fallbackGroupId, revision) → `GridLayer`
|
||
(templateId, name, priority, applicability jsonb, isEnabled) → `Slot` (все поля из 3.4, включая
|
||
`slotKind`, `snapToMinutes`, `overflowPolicy`, `isAnchor`). `SlotState` (см. 3.6).
|
||
|
||
В этом срезе используются только базовый слой и фоновый; применимость слоёв — в срезе 3.
|
||
|
||
Готово: модель и миграция; у канала при создании появляется шаблон с фоновым слоем.
|
||
|
||
#### 1.10. Изменения в `Channel`
|
||
`Domain` `Infrastructure` `Application` `Api`
|
||
|
||
Добавить `utcOffsetMinutes` (дефолт 180), `dayStartTime` (дефолт 06:00), `templateId`, `number`.
|
||
Обновить создание канала и команду настроек.
|
||
|
||
Готово: канал создаётся с шаблоном, оффсетом и началом вещательных суток.
|
||
|
||
#### 1.11. CRUD слоёв и слотов
|
||
`Application` `Api`
|
||
|
||
Создание/правка/удаление слотов, проверка пересечений внутри слоя, валидация кратности времён,
|
||
проверка что группа существует и не пуста. Пометка шаблона изменённым (`revision`) при любой правке.
|
||
|
||
Готово: сетку можно собрать через API, некорректный слот не сохраняется.
|
||
|
||
### 1D. Генератор
|
||
|
||
#### 1.12. Разрешение сетки
|
||
`Domain`
|
||
|
||
Чистая функция: на момент времени вернуть активный слот — применимые слои по убыванию приоритета,
|
||
первый слот, накрывающий момент. Учёт вещательных суток (`dayStartTime`) при определении дня недели.
|
||
Юнит-тесты на границы суток и перекрытие слоёв.
|
||
|
||
Готово: `tests/TeleWave.Domain.Tests` покрывает выбор слота, включая ночь после полуночи.
|
||
|
||
#### 1.13. Стратегии выбора
|
||
`Domain`
|
||
|
||
`sequential`, `randomWithCooldown`, `fixed` (см. 3.5). Внутри элемента единицы всегда по порядку
|
||
от курсора. Разворачивание элемента в последовательность единиц: сериал → серии, коллекция →
|
||
фильмы (сериал внутри коллекции разворачивается), одиночное шоу → одна единица (см. 3.2).
|
||
|
||
Готово: юнит-тесты на каждую стратегию, включая заворот в конце и пустую группу.
|
||
|
||
#### 1.14. Наполнение слота
|
||
`Domain`
|
||
|
||
Бюджет по `blockMode` (`count` / `duration` / `fillSlot`), `overflowPolicy`, продвижение `SlotState`.
|
||
Пока без стыков и якорей — контент встык.
|
||
|
||
Готово: юнит-тесты на все три режима блока и три политики переполнения.
|
||
|
||
#### 1.15. Фоновый слой и филлер
|
||
`Domain`
|
||
|
||
Незакрытые интервалы отдаются слоту фонового слоя; если пуст и он — `fallbackGroupId` шаблона;
|
||
если пуст и он — зацикленный аварийный ассет канала. Все длительности округляются вверх до сегмента
|
||
(см. 2.2).
|
||
|
||
Готово: дыр в ленте не бывает; юнит-тест на канал с пустой группой.
|
||
|
||
#### 1.16. Оркестратор генерации
|
||
`Application`
|
||
|
||
Новый генератор по образцу `ScheduleGenerator`: загрузка конфигурации, вызов чистой части,
|
||
материализация с записью `trace` (см. 3.9), продвижение `SlotState`, advisory-lock канала,
|
||
транзакция. Горизонт из `SchedulerOptions.HorizonDays` (поднять дефолт до 7).
|
||
|
||
Готово: команда генерации наполняет расписание канала на неделю вперёд.
|
||
|
||
#### 1.17. Ретеншн и обслуживание
|
||
`Application` `Infrastructure`
|
||
|
||
`SchedulerOptions.RetentionHours` → `RetentionDays` (дефолт 90). Фоновая задача обслуживания
|
||
(см. 3.6): чистка записей сверх ретеншна, осиротевших `BumperAsset`, временных файлов.
|
||
Валидация: правило требует истории глубже, чем хранится → предупреждение.
|
||
|
||
Готово: старые записи и неиспользуемые заставки удаляются по расписанию.
|
||
|
||
#### 1.18. Применение по кнопке
|
||
`Application` `Api`
|
||
|
||
Правка правил не трогает эфир, только помечает шаблон изменённым. Отдельная команда «применить»
|
||
пересобирает хвост от `now` (см. 4.5). В ответ — сколько записей затронуто.
|
||
|
||
Готово: сохранение слота эфир не двигает; применение пересобирает будущее и идемпотентно.
|
||
|
||
#### 1.19. Золотые тесты генератора
|
||
`Domain.Tests`
|
||
|
||
Набор эталонных конфигураций (канал с одним слоем, с фоном, с коллекцией, с пустой группой) и
|
||
зафиксированные сутки эфира. Сравнение посуточной ленты целиком, обновление эталона — явной
|
||
командой.
|
||
|
||
Готово: правка генератора показывает построчный диф вместо «тесты упали».
|
||
|
||
### 1E. Снос старого
|
||
|
||
#### 1.20. Удаление ротации
|
||
`Domain` `Infrastructure` `Application` `Api` `Frontend`
|
||
|
||
Удалить `ChannelShow`, `ChannelShowHour`, `ChannelAd`, `ProgrammingOverride`, `OverrideShow`,
|
||
`AdInsertion`, `OverrideMode`, `OverrideRecurrence`, `SchedulePlanner` и все `Planner*`-модели,
|
||
соответствующие команды и эндпоинты, компоненты `AddShowForm`, `AddAdForm`, `ChannelShowRow`,
|
||
`OverrideForm` (см. 9).
|
||
|
||
Реклама и заставки на этом шаге временно генерируются старым способом — переезжают в стыки в срезе 2.
|
||
|
||
Готово: старый планировщик удалён, сборка и тесты зелёные.
|
||
|
||
### 1F. UI
|
||
|
||
#### 1.21. Read-only сетка и инспектор слота
|
||
`Frontend`
|
||
|
||
Недельная сетка (ось X — дни, ось Y — время от `dayStartTime`), пока без перетаскивания: добавление
|
||
слота кнопкой на пустом месте, правка в инспекторе (см. 6.1). Индикатор «правила изменены» с кнопкой
|
||
применения. Экран `ChannelDetail` переписывается.
|
||
|
||
Готово: канал настраивается и вещает по сетке целиком через новый UI.
|
||
|
||
---
|
||
|
||
## Срез 2 — врезки
|
||
|
||
#### 2.1. Ролики как `Show(Kind = Interstitial)`
|
||
`Domain` `Infrastructure` `Application`
|
||
|
||
Новое значение `ShowKind`. Исключение таких шоу из обычного списка библиотеки и из запросов
|
||
метаданных.
|
||
|
||
#### 2.2. Раздел «Ролики»
|
||
`Api` `Frontend`
|
||
|
||
Отдельный экран (см. 6.7): плоский список с длительностями и превью, массовая загрузка, сборка
|
||
блока перетаскиванием с показом суммарной длительности (сохраняется как коллекция), группы роликов.
|
||
|
||
#### 2.3. Модель шаблона стыка
|
||
`Domain` `Infrastructure`
|
||
|
||
`JunctionTemplate` + `JunctionElement` (см. 3.7), включая `amountMode` (`count` / `duration`) и
|
||
структурированные `conditions`. Ссылки `junctionBetweenId` / `junctionAfterId` на слоте.
|
||
|
||
#### 2.4. Раскладка врезок
|
||
`Domain`
|
||
|
||
Наполнение стыка: обязательные элементы, набор по бюджету, проверка условий, округление до сегмента.
|
||
Коллекция входит целиком и не разрезается. Юнит-тесты на смешанную группу блоков и одиночных роликов.
|
||
|
||
#### 2.5. Переезд заставок в стык
|
||
`Application`
|
||
|
||
`BumperTemplate` / `ScheduleBumperResolver` и рендер не меняются — меняется точка вызова: вместо
|
||
настроек канала (`BumperSelection`, `NextBumperIndex`, `BumperMinIntervalMinutes`, обе `*Chance`)
|
||
условия берутся из элемента стыка. Перечисленные поля канала удаляются, `BumperFont` остаётся.
|
||
|
||
#### 2.6. Якоря и округление стартов
|
||
`Domain`
|
||
|
||
Жёсткие старты якорных слотов (не начинать единицу, перелезающую через якорь), мягкое округление
|
||
`snapToMinutes` с отказом при превышении `maxDriftMinutes` (см. 4.3). Юнит-тесты на оба механизма
|
||
и на их сочетание.
|
||
|
||
#### 2.7. Редактор стыка
|
||
`Frontend`
|
||
|
||
Горизонтальная цепочка с перетаскиванием элементов, линейка суммарной длительности, попап параметров
|
||
(см. 6.3).
|
||
|
||
#### 2.8. Предпросмотр
|
||
`Application` `Api` `Frontend`
|
||
|
||
Прогон генератора на черновике правил **без записи и без продвижения курсоров**. Заставки —
|
||
плейсхолдерами известной длины, реальный рендер только при применении (см. 3.7). Вкладки «Программа»
|
||
и «Лента» с гистограммой нагрузки врезок по часам.
|
||
|
||
---
|
||
|
||
## Срез 3 — гибкость и календарь
|
||
|
||
#### 3.1. Применимость слоёв
|
||
`Domain` `Infrastructure` `Application`
|
||
|
||
`applicability`: дни недели, разовые диапазоны дат, ежегодные диапазоны, конкретные даты (см. 3.4).
|
||
Приоритеты, разрешение конфликтов. Юнит-тесты на пересекающиеся слои и на границу года в ежегодном
|
||
диапазоне.
|
||
|
||
#### 3.2. Фильтры кандидатов
|
||
`Domain`
|
||
|
||
`maxAudienceByTime`, `cooldown`, `maxRepeatsInWindow` (см. 3.8) — применяются **до** взвешенного
|
||
выбора. История берётся из материализованной ленты. `fallback` стратегии, когда остывание отсекло
|
||
всех.
|
||
|
||
#### 3.3. Слот `repeat`
|
||
`Domain` `Application`
|
||
|
||
Чтение уже записанной ленты по `repeatSource` (daysAgo, time, durationMinutes) и перенос найденных
|
||
программ. Источник пуст → фон + предупреждение.
|
||
|
||
#### 3.4. Слот `signOff`
|
||
`Domain` `Application` `Frontend`
|
||
|
||
Конец вещания: заполнение зацикленным ассетом, пометка в EPG «эфир не ведётся», отображение
|
||
в программе.
|
||
|
||
#### 3.5. Панель слоёв
|
||
`Frontend`
|
||
|
||
Список слоёв с видимостью и приоритетом, drag для переупорядочивания, выбор редактируемого слоя,
|
||
штриховка перекрытых слотов, переключатель даты («показать сетку на 25 декабря»).
|
||
|
||
#### 3.6. Drag & drop в календаре
|
||
`Frontend`
|
||
|
||
Перетаскивание слотов и изменение длительности за края, копирование дня на другие дни, копирование
|
||
недели. Своя реализация — готовые календари не покрывают слои, вещательные сутки и якоря (см. 6.1).
|
||
|
||
---
|
||
|
||
## Срез 4 — эксплуатация
|
||
|
||
#### 4.1. Валидация до генерации
|
||
`Application` `Api` `Frontend`
|
||
|
||
Проверки из 5.1: нехватка контента, пустая группа, дыра в сетке, пересечение слотов, недостижимый
|
||
кулдаун, возрастной конфликт. Отдаются вместе с шаблоном, показываются в редакторе.
|
||
|
||
#### 4.2. Пост-проверки
|
||
`Application`
|
||
|
||
Потолок врезок в час, доля жанра за сутки, превышение дрейфа, доля эфира у фона (см. 3.8, 5.2).
|
||
Предупреждения, не ошибки — ничего не переигрывается.
|
||
|
||
#### 4.3. Вкладка «Проблемы» и тепловая карта
|
||
`Frontend`
|
||
|
||
Сгруппированные предупреждения с переходом к источнику; матрица «элемент × день» с яркостью
|
||
по числу показов.
|
||
|
||
#### 4.4. «Почему это здесь»
|
||
`Api` `Frontend`
|
||
|
||
Отдача `trace` записи и экран цепочки происхождения (см. 6.5).
|
||
|
||
#### 4.5. Диф перед применением
|
||
`Application` `Api` `Frontend`
|
||
|
||
Сравнение текущего хвоста с пересчитанным, список изменений, отдельная подсветка ближайших суток
|
||
(см. 6.6).
|
||
|
||
#### 4.6. Копирование шаблона
|
||
`Application` `Api` `Frontend`
|
||
|
||
Глубокая копия шаблона, слоёв и слотов на другой канал; группы не копируются, они общие.
|
||
|
||
---
|
||
|
||
## Смежное — зрительская часть
|
||
|
||
От планировщика не зависит, делается параллельно в любой момент. Всё опционально и по умолчанию
|
||
выключено (см. 6.8).
|
||
|
||
#### V.1. Номер канала
|
||
`Domain` `Application` `Api` `Frontend`
|
||
|
||
`Channel.number`, уникальность, сортировка публичного списка по номеру.
|
||
|
||
#### V.2. Переключение по номерам
|
||
`Frontend`
|
||
|
||
Вверх-вниз по номерам, короткий чёрный кадр, номер в углу на секунду. Включается глобальным флагом
|
||
в `AppSetting` рядом с флагом регистрации. Сетка каналов остаётся вторым способом навигации.
|
||
|
||
#### V.3. Логотип канала
|
||
`Domain` `Api` `Frontend`
|
||
|
||
`logoImageId` (реестр изображений), угол и прозрачность. Оверлей поверх `<video>`, без касания
|
||
ffmpeg.
|
||
|
||
#### V.4. Часы и плашка «Далее»
|
||
`Frontend`
|
||
|
||
Часы — опция канала. Плашка в конце программы по данным EPG.
|
||
|
||
#### V.5. Аналоговый фильтр
|
||
`Domain` `Frontend`
|
||
|
||
`analogFilterStrength` на канале, CSS-фильтр или шейдер, по умолчанию выключен.
|
||
|
||
---
|
||
|
||
## Порядок и параллельность
|
||
|
||
- **1A** (библиотека) и **1B** (группы) можно вести параллельно; **1C** зависит от 1B.
|
||
- **1D** (генератор) зависит от 1C, но чистые части (1.12–1.15) пишутся и тестируются без БД.
|
||
- **1E** (снос) выполняется только после 1.16 — иначе канал остаётся без эфира.
|
||
- **Срез 2** целиком зависит от среза 1; внутри него 2.1–2.2 (ролики) независимы от 2.3–2.7 (стыки).
|
||
- **Смежное** не зависит ни от чего, кроме V.2, которому нужен V.1.
|