Enhance GridScheduleGenerator and related components for improved slot handling and cursor management
ci / build-backend (push) Successful in 1m21s
ci / build-frontend (push) Successful in 51s
ci / tests (push) Successful in 1m40s
ci / sonar (push) Successful in 3m46s

Updated the GridScheduleGenerator to support rebuilding future schedules while correctly managing aired positions. Introduced a new AiredPosition record to track the last aired state of slots, ensuring that the cursor rewinds to the correct position during rebuilds. Refactored the BuildInputAsync method to accept aired positions, and modified the LoadAiredPositionsAsync method for accurate retrieval of past entries. Enhanced the SchedulePlanner to utilize shared cursor states between main and background loops, preventing duplicate series plays. Updated documentation to reflect these changes and added integration tests to verify the correct behavior of the new functionality.
This commit is contained in:
Leonid Pershin
2026-07-29 08:57:46 +03:00
parent c67d1f7752
commit fbfc6e677e
11 changed files with 568 additions and 117 deletions
+23 -3
View File
@@ -409,9 +409,27 @@ SlotState
Курсор хранит **ссылку на элемент**, а не числовой индекс в группе: при удалении позиции из группы
курсор корректно переезжает на следующую, а не сдвигает всё.
**Курсор один на слот в пределах прогона.** `SlotState` — это снимок на начало прогона, а слот
попадает в планировщик по экземпляру на каждые свои вещательные сутки; горизонт в неделю строится
целиком. Позиция поэтому живёт в накопителе прогона, а снимок только задаёт её начальное значение.
Иначе вторник начинался бы ровно с того места, что и понедельник. По той же причине позиция общая
с фоновым слоем: слот, перекрытый в одни сутки и свободный в другие, идёт то основным циклом, то
в паузах, и второй счётчик переигрывал бы уже поставленные серии. Берётся позиция в момент выбора
элемента, а не при входе в слот: паузу перед стартом слота закрывает фон, и он мог сдвинуть её
только что.
**История показов для остывания** берётся из материализованного расписания (`ScheduleEntry` с
`Kind = Program`, индекс по `channelId + showId + startsAtUtc`), отдельного журнала не заводим — одна
правда, и при пересборке хвоста будущие показы удаляются вместе с записями.
правда, и при пересборке хвоста будущие показы удаляются вместе с записями. К ней **прибавляются
показы текущего прогона**: лента на момент сборки входа содержит только прошлое, и без этого
остывание с потолком повторов не видели бы собственный горизонт — на свежем канале оба правила
не срабатывали бы ни разу.
**Пересборка отматывает курсор к отыгранному.** Снос будущего хвоста выбрасывает выходы, которые
курсор уже прошёл; без отмотки каждое применение проматывало бы библиотеку на горизонт вперёд,
теряя серии, которые так и не вышли. Точка берётся по последней уцелевшей записи каждого слота:
элемент — из её трейса, позиция — поиском единицы в развёрнутом элементе (у коллекции серии
нумеруются внутри каждой части, и `episodeIndex` как индекс в общей последовательности не годится).
Из этого следует: `SchedulerOptions.RetentionHours` (сейчас 24 часа) заменяется на `RetentionDays`,
дефолт 90. Значение обязано покрывать максимальный `cooldownDays` среди правил канала и максимальный
@@ -829,8 +847,10 @@ seed = hash(channelId, date, slotId, occurrenceInDay)
### 5.2. После генерации
- элементы, отданные фону (не нашлось контента);
- пост-проверки из 3.8;
- элементы, отданные фону (не нашлось контента, либо источник повтора не поместился в слот целиком —
снаружи это неотличимо от пустого источника, а чинится совсем другим);
- пост-проверки из 3.8 — доли считаются только по вещательным суткам, попавшим в прогон целиком:
на краях горизонта сутки обрезаны, и один фильм честно занимает в таком огрызке больше половины;
- тепловая карта повторов: матрица «элемент × день», яркость = число показов — сразу видно, что один
фильм крутится четыре раза за неделю;
- фактическая доля врезок по часам против лимита.