Files
TeleWave/docs/tv-scheduler-tasks.md
T

23 KiB
Raw Blame History

Планировщик эфира: задачи

Разбивка 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.RetentionHoursRetentionDays (дефолт 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.