Normalize line endings to LF via .gitattributes
Репозиторий хранил фронтенд в CRLF, а часть бэкенда — вперемешку, хотя CI и Docker-сборка работают под Linux. Прибиваем LF атрибутом `* text=auto eol=lf` и разово нормализуем дерево, чтобы форматтеры не переписывали файлы целиком на каждом прогоне. Коммит чисто механический: изменений содержимого нет, только концы строк. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9d1c6d2fc3
commit
0442056367
@@ -1,221 +1,221 @@
|
||||
using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Domain.Programming.Planning;
|
||||
|
||||
/// <summary>
|
||||
/// Единица воспроизведения — серия или фильм с готовым ассетом. Планировщик оперирует ими, а не
|
||||
/// шоу: у фильма единица одна, у сериала их столько же, сколько серий, у коллекции — сумма по частям.
|
||||
/// </summary>
|
||||
public sealed record PlanningUnit(Guid MediaAssetId, TimeSpan Duration, Guid ShowId, int UnitIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Элемент группы, развёрнутый в последовательность единиц. <see cref="LastPlayedUtc"/> — когда он
|
||||
/// в последний раз выходил в этом канале; по нему работает остывание.
|
||||
/// </summary>
|
||||
public sealed record PlanningElement(
|
||||
GroupElementKind Kind,
|
||||
Guid ElementId,
|
||||
int Weight,
|
||||
int Position,
|
||||
IReadOnlyList<PlanningUnit> Units,
|
||||
DateTimeOffset? LastPlayedUtc = null,
|
||||
/// <summary>Категория аудитории (у коллекции — строжайшая из частей); по ней работает детское время.</summary>
|
||||
ShowAudience? Audience = null,
|
||||
/// <summary>Старты недавних показов в этом канале — по ним считается потолок повторов за период.</summary>
|
||||
IReadOnlyList<DateTimeOffset>? RecentPlaysUtc = null
|
||||
);
|
||||
|
||||
/// <summary>Потолок повторов: не чаще <paramref name="Max"/> раз за <paramref name="WindowDays"/> суток.</summary>
|
||||
public sealed record RepeatLimit(int WindowDays, int Max);
|
||||
|
||||
/// <summary>Стратегия выбора элемента, приведённая к виду, понятному чистому планировщику.</summary>
|
||||
public sealed record PlanningStrategy(
|
||||
SlotStrategyKind Kind,
|
||||
bool RestartOnEnd = true,
|
||||
int CooldownDays = 0,
|
||||
bool IgnoreCooldownWhenExhausted = false,
|
||||
Guid? FixedElementId = null
|
||||
);
|
||||
|
||||
/// <summary>Как слот выбирает элемент. Дублирует прикладной enum, чтобы домен не зависел от Application.</summary>
|
||||
public enum SlotStrategyKind
|
||||
{
|
||||
Sequential = 0,
|
||||
RandomWithCooldown = 1,
|
||||
Fixed = 2,
|
||||
}
|
||||
|
||||
/// <summary>Где остановились в текущем элементе на момент начала прогона.</summary>
|
||||
public sealed record PlanningCursor(
|
||||
GroupElementKind? ElementKind,
|
||||
Guid? ElementId,
|
||||
int NextUnitIndex
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Слот, привязанный к конкретному моменту эфира. Применимость слоёв уже разрешена: сюда попадают
|
||||
/// только те слоты, которые реально действуют в эти сутки, каждый со своим целевым временем в UTC.
|
||||
/// </summary>
|
||||
public sealed record PlanningSlot(
|
||||
Guid SlotId,
|
||||
DateTimeOffset TargetStartUtc,
|
||||
int TargetDurationMinutes,
|
||||
SlotKind SlotKind,
|
||||
bool IsAnchor,
|
||||
int MaxDriftMinutes,
|
||||
int? SnapToMinutes,
|
||||
SlotBlockMode BlockMode,
|
||||
int BlockValue,
|
||||
OverflowPolicy OverflowPolicy,
|
||||
PlanningStrategy Strategy,
|
||||
IReadOnlyList<PlanningElement> Elements,
|
||||
PlanningCursor? Cursor,
|
||||
/// <summary>Готовые записи для <see cref="SlotKind.Repeat"/> — что играло в источнике повтора.</summary>
|
||||
IReadOnlyList<PlanningUnit>? RepeatUnits = null,
|
||||
/// <summary>Врезки между единицами внутри блока.</summary>
|
||||
PlanningJunction? JunctionBetween = null,
|
||||
/// <summary>Врезки в конце блока.</summary>
|
||||
PlanningJunction? JunctionAfter = null,
|
||||
/// <summary>Возрастной потолок в это время суток (null — без ограничения). Жёсткий фильтр.</summary>
|
||||
ShowAudience? MaxAudience = null,
|
||||
/// <summary>Потолок повторов за период (null — без ограничения). Жёсткий фильтр.</summary>
|
||||
RepeatLimit? RepeatLimit = null
|
||||
)
|
||||
{
|
||||
public DateTimeOffset TargetEndUtc => TargetStartUtc.AddMinutes(TargetDurationMinutes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Врезка стыка, развёрнутая для планировщика: единицы уже подобраны оркестратором, домену остаётся
|
||||
/// решить, сколько их поставить и влезают ли они.
|
||||
/// </summary>
|
||||
public sealed record PlanningJunctionElement(
|
||||
JunctionElementKind Kind,
|
||||
IReadOnlyList<PlanningUnit> Units,
|
||||
JunctionAmountMode AmountMode,
|
||||
int AmountValue,
|
||||
bool IsRequired,
|
||||
/// <summary>Ставить только при смене элемента (иначе — и между единицами одного).</summary>
|
||||
bool OnlyOnElementChange = false,
|
||||
/// <summary>Не ставить чаще, чем раз в N минут (0 — без ограничения).</summary>
|
||||
int MinMinutesBetween = 0,
|
||||
/// <summary>Блок заставки — ассет рендерится позже, планировщик резервирует длительность.</summary>
|
||||
Guid? BumperTemplateId = null,
|
||||
/// <summary>Длительность резерва под заставку.</summary>
|
||||
TimeSpan BumperDuration = default
|
||||
);
|
||||
|
||||
/// <summary>Стык: последовательность врезок между программами.</summary>
|
||||
public sealed record PlanningJunction(
|
||||
Guid JunctionId,
|
||||
IReadOnlyList<PlanningJunctionElement> Elements
|
||||
);
|
||||
|
||||
/// <summary>Полный вход одного прогона генератора.</summary>
|
||||
public sealed record PlanningInput(
|
||||
Guid ChannelId,
|
||||
DateTimeOffset StartUtc,
|
||||
DateTimeOffset HorizonEndUtc,
|
||||
IReadOnlyList<PlanningSlot> Slots,
|
||||
/// <summary>Чем закрывать место, не покрытое слотами и не заполненное контентом.</summary>
|
||||
IReadOnlyList<PlanningUnit> FallbackUnits,
|
||||
int SegmentSeconds
|
||||
);
|
||||
|
||||
/// <summary>Одна запись будущей ленты. Трейс пишется здесь же — восстановить его потом невозможно.</summary>
|
||||
public sealed record PlannedItem(
|
||||
Guid MediaAssetId,
|
||||
DateTimeOffset StartsAtUtc,
|
||||
DateTimeOffset EndsAtUtc,
|
||||
Guid? ShowId,
|
||||
int? UnitIndex,
|
||||
Guid? SlotId,
|
||||
PlannedItemKind Kind,
|
||||
PlanTrace? Trace = null,
|
||||
/// <summary>Для заставки: блок, пара «из/в» и место под ассет, который отрендерят позже.</summary>
|
||||
Guid? BumperTemplateId = null,
|
||||
Guid? FromShowId = null,
|
||||
Guid? ToShowId = null,
|
||||
/// <summary>Коллекция, частью которой шла единица (null — шоу играло само по себе).</summary>
|
||||
Guid? CollectionId = null
|
||||
);
|
||||
|
||||
public enum PlannedItemKind
|
||||
{
|
||||
Program = 0,
|
||||
Fallback = 1,
|
||||
SignOff = 2,
|
||||
Ad = 3,
|
||||
Promo = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Заставка-переход. Ассет пуст: он зависит от пары соседей и рендерится после того, как лента
|
||||
/// собрана, — планировщик лишь резервирует под неё длительность.
|
||||
/// </summary>
|
||||
Bumper = 5,
|
||||
}
|
||||
|
||||
/// <summary>Цепочка происхождения записи — питает экран «почему это здесь».</summary>
|
||||
public sealed record PlanTrace(
|
||||
Guid? SlotId,
|
||||
SlotKind SlotKind,
|
||||
GroupElementKind? ElementKind,
|
||||
Guid? ElementId,
|
||||
SlotStrategyKind? Strategy,
|
||||
/// <summary>Сколько кандидатов осталось после остывания (null — выбор без остывания).</summary>
|
||||
int? CandidatesAfterCooldown,
|
||||
/// <summary>Насколько фактический старт разошёлся с целевым, минуты.</summary>
|
||||
int DriftMinutes,
|
||||
/// <summary>Старт сдвинут вперёд округлением до круглого времени.</summary>
|
||||
bool Snapped
|
||||
);
|
||||
|
||||
/// <summary>Новое состояние слота после прогона — оркестратор сохраняет его в БД.</summary>
|
||||
public sealed record PlanningCursorUpdate(
|
||||
Guid SlotId,
|
||||
GroupElementKind? ElementKind,
|
||||
Guid? ElementId,
|
||||
int NextUnitIndex
|
||||
);
|
||||
|
||||
/// <summary>Результат прогона: лента, новые курсоры и предупреждения для админа.</summary>
|
||||
public sealed record PlanningResult(
|
||||
IReadOnlyList<PlannedItem> Items,
|
||||
IReadOnlyList<PlanningCursorUpdate> Cursors,
|
||||
IReadOnlyList<PlanningWarning> Warnings
|
||||
);
|
||||
|
||||
/// <summary>Предупреждение по результату генерации: не ошибка, но админу это видеть нужно.</summary>
|
||||
public sealed record PlanningWarning(PlanningWarningKind Kind, Guid? SlotId, string Details);
|
||||
|
||||
public enum PlanningWarningKind
|
||||
{
|
||||
/// <summary>Слот не дал контента — место закрыл фон.</summary>
|
||||
SlotEmpty = 0,
|
||||
|
||||
/// <summary>Фактический старт ушёл дальше допуска.</summary>
|
||||
DriftExceeded = 1,
|
||||
|
||||
/// <summary>Остывание отсекло всех кандидатов.</summary>
|
||||
CooldownExhausted = 2,
|
||||
|
||||
/// <summary>Не нашлось, что повторить.</summary>
|
||||
RepeatSourceEmpty = 3,
|
||||
|
||||
/// <summary>Пусто даже в фоне — в ленте образуется дыра.</summary>
|
||||
FallbackEmpty = 4,
|
||||
|
||||
/// <summary>Жёсткие фильтры (детское время, потолок повторов) не оставили ни одного кандидата.</summary>
|
||||
CandidatesFiltered = 5,
|
||||
|
||||
// ── Пост-проверки: считаются по готовой ленте и ничего не переигрывают (см. 3.8). ──
|
||||
|
||||
/// <summary>Врезок в часе больше заданного потолка.</summary>
|
||||
BreakLimitExceeded = 6,
|
||||
|
||||
/// <summary>Доля одного жанра за сутки выше заданной.</summary>
|
||||
GenreShareExceeded = 7,
|
||||
|
||||
/// <summary>Фон занял больше эфира, чем считается нормой.</summary>
|
||||
FallbackShareExceeded = 8,
|
||||
}
|
||||
using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Domain.Programming.Planning;
|
||||
|
||||
/// <summary>
|
||||
/// Единица воспроизведения — серия или фильм с готовым ассетом. Планировщик оперирует ими, а не
|
||||
/// шоу: у фильма единица одна, у сериала их столько же, сколько серий, у коллекции — сумма по частям.
|
||||
/// </summary>
|
||||
public sealed record PlanningUnit(Guid MediaAssetId, TimeSpan Duration, Guid ShowId, int UnitIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Элемент группы, развёрнутый в последовательность единиц. <see cref="LastPlayedUtc"/> — когда он
|
||||
/// в последний раз выходил в этом канале; по нему работает остывание.
|
||||
/// </summary>
|
||||
public sealed record PlanningElement(
|
||||
GroupElementKind Kind,
|
||||
Guid ElementId,
|
||||
int Weight,
|
||||
int Position,
|
||||
IReadOnlyList<PlanningUnit> Units,
|
||||
DateTimeOffset? LastPlayedUtc = null,
|
||||
/// <summary>Категория аудитории (у коллекции — строжайшая из частей); по ней работает детское время.</summary>
|
||||
ShowAudience? Audience = null,
|
||||
/// <summary>Старты недавних показов в этом канале — по ним считается потолок повторов за период.</summary>
|
||||
IReadOnlyList<DateTimeOffset>? RecentPlaysUtc = null
|
||||
);
|
||||
|
||||
/// <summary>Потолок повторов: не чаще <paramref name="Max"/> раз за <paramref name="WindowDays"/> суток.</summary>
|
||||
public sealed record RepeatLimit(int WindowDays, int Max);
|
||||
|
||||
/// <summary>Стратегия выбора элемента, приведённая к виду, понятному чистому планировщику.</summary>
|
||||
public sealed record PlanningStrategy(
|
||||
SlotStrategyKind Kind,
|
||||
bool RestartOnEnd = true,
|
||||
int CooldownDays = 0,
|
||||
bool IgnoreCooldownWhenExhausted = false,
|
||||
Guid? FixedElementId = null
|
||||
);
|
||||
|
||||
/// <summary>Как слот выбирает элемент. Дублирует прикладной enum, чтобы домен не зависел от Application.</summary>
|
||||
public enum SlotStrategyKind
|
||||
{
|
||||
Sequential = 0,
|
||||
RandomWithCooldown = 1,
|
||||
Fixed = 2,
|
||||
}
|
||||
|
||||
/// <summary>Где остановились в текущем элементе на момент начала прогона.</summary>
|
||||
public sealed record PlanningCursor(
|
||||
GroupElementKind? ElementKind,
|
||||
Guid? ElementId,
|
||||
int NextUnitIndex
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Слот, привязанный к конкретному моменту эфира. Применимость слоёв уже разрешена: сюда попадают
|
||||
/// только те слоты, которые реально действуют в эти сутки, каждый со своим целевым временем в UTC.
|
||||
/// </summary>
|
||||
public sealed record PlanningSlot(
|
||||
Guid SlotId,
|
||||
DateTimeOffset TargetStartUtc,
|
||||
int TargetDurationMinutes,
|
||||
SlotKind SlotKind,
|
||||
bool IsAnchor,
|
||||
int MaxDriftMinutes,
|
||||
int? SnapToMinutes,
|
||||
SlotBlockMode BlockMode,
|
||||
int BlockValue,
|
||||
OverflowPolicy OverflowPolicy,
|
||||
PlanningStrategy Strategy,
|
||||
IReadOnlyList<PlanningElement> Elements,
|
||||
PlanningCursor? Cursor,
|
||||
/// <summary>Готовые записи для <see cref="SlotKind.Repeat"/> — что играло в источнике повтора.</summary>
|
||||
IReadOnlyList<PlanningUnit>? RepeatUnits = null,
|
||||
/// <summary>Врезки между единицами внутри блока.</summary>
|
||||
PlanningJunction? JunctionBetween = null,
|
||||
/// <summary>Врезки в конце блока.</summary>
|
||||
PlanningJunction? JunctionAfter = null,
|
||||
/// <summary>Возрастной потолок в это время суток (null — без ограничения). Жёсткий фильтр.</summary>
|
||||
ShowAudience? MaxAudience = null,
|
||||
/// <summary>Потолок повторов за период (null — без ограничения). Жёсткий фильтр.</summary>
|
||||
RepeatLimit? RepeatLimit = null
|
||||
)
|
||||
{
|
||||
public DateTimeOffset TargetEndUtc => TargetStartUtc.AddMinutes(TargetDurationMinutes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Врезка стыка, развёрнутая для планировщика: единицы уже подобраны оркестратором, домену остаётся
|
||||
/// решить, сколько их поставить и влезают ли они.
|
||||
/// </summary>
|
||||
public sealed record PlanningJunctionElement(
|
||||
JunctionElementKind Kind,
|
||||
IReadOnlyList<PlanningUnit> Units,
|
||||
JunctionAmountMode AmountMode,
|
||||
int AmountValue,
|
||||
bool IsRequired,
|
||||
/// <summary>Ставить только при смене элемента (иначе — и между единицами одного).</summary>
|
||||
bool OnlyOnElementChange = false,
|
||||
/// <summary>Не ставить чаще, чем раз в N минут (0 — без ограничения).</summary>
|
||||
int MinMinutesBetween = 0,
|
||||
/// <summary>Блок заставки — ассет рендерится позже, планировщик резервирует длительность.</summary>
|
||||
Guid? BumperTemplateId = null,
|
||||
/// <summary>Длительность резерва под заставку.</summary>
|
||||
TimeSpan BumperDuration = default
|
||||
);
|
||||
|
||||
/// <summary>Стык: последовательность врезок между программами.</summary>
|
||||
public sealed record PlanningJunction(
|
||||
Guid JunctionId,
|
||||
IReadOnlyList<PlanningJunctionElement> Elements
|
||||
);
|
||||
|
||||
/// <summary>Полный вход одного прогона генератора.</summary>
|
||||
public sealed record PlanningInput(
|
||||
Guid ChannelId,
|
||||
DateTimeOffset StartUtc,
|
||||
DateTimeOffset HorizonEndUtc,
|
||||
IReadOnlyList<PlanningSlot> Slots,
|
||||
/// <summary>Чем закрывать место, не покрытое слотами и не заполненное контентом.</summary>
|
||||
IReadOnlyList<PlanningUnit> FallbackUnits,
|
||||
int SegmentSeconds
|
||||
);
|
||||
|
||||
/// <summary>Одна запись будущей ленты. Трейс пишется здесь же — восстановить его потом невозможно.</summary>
|
||||
public sealed record PlannedItem(
|
||||
Guid MediaAssetId,
|
||||
DateTimeOffset StartsAtUtc,
|
||||
DateTimeOffset EndsAtUtc,
|
||||
Guid? ShowId,
|
||||
int? UnitIndex,
|
||||
Guid? SlotId,
|
||||
PlannedItemKind Kind,
|
||||
PlanTrace? Trace = null,
|
||||
/// <summary>Для заставки: блок, пара «из/в» и место под ассет, который отрендерят позже.</summary>
|
||||
Guid? BumperTemplateId = null,
|
||||
Guid? FromShowId = null,
|
||||
Guid? ToShowId = null,
|
||||
/// <summary>Коллекция, частью которой шла единица (null — шоу играло само по себе).</summary>
|
||||
Guid? CollectionId = null
|
||||
);
|
||||
|
||||
public enum PlannedItemKind
|
||||
{
|
||||
Program = 0,
|
||||
Fallback = 1,
|
||||
SignOff = 2,
|
||||
Ad = 3,
|
||||
Promo = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Заставка-переход. Ассет пуст: он зависит от пары соседей и рендерится после того, как лента
|
||||
/// собрана, — планировщик лишь резервирует под неё длительность.
|
||||
/// </summary>
|
||||
Bumper = 5,
|
||||
}
|
||||
|
||||
/// <summary>Цепочка происхождения записи — питает экран «почему это здесь».</summary>
|
||||
public sealed record PlanTrace(
|
||||
Guid? SlotId,
|
||||
SlotKind SlotKind,
|
||||
GroupElementKind? ElementKind,
|
||||
Guid? ElementId,
|
||||
SlotStrategyKind? Strategy,
|
||||
/// <summary>Сколько кандидатов осталось после остывания (null — выбор без остывания).</summary>
|
||||
int? CandidatesAfterCooldown,
|
||||
/// <summary>Насколько фактический старт разошёлся с целевым, минуты.</summary>
|
||||
int DriftMinutes,
|
||||
/// <summary>Старт сдвинут вперёд округлением до круглого времени.</summary>
|
||||
bool Snapped
|
||||
);
|
||||
|
||||
/// <summary>Новое состояние слота после прогона — оркестратор сохраняет его в БД.</summary>
|
||||
public sealed record PlanningCursorUpdate(
|
||||
Guid SlotId,
|
||||
GroupElementKind? ElementKind,
|
||||
Guid? ElementId,
|
||||
int NextUnitIndex
|
||||
);
|
||||
|
||||
/// <summary>Результат прогона: лента, новые курсоры и предупреждения для админа.</summary>
|
||||
public sealed record PlanningResult(
|
||||
IReadOnlyList<PlannedItem> Items,
|
||||
IReadOnlyList<PlanningCursorUpdate> Cursors,
|
||||
IReadOnlyList<PlanningWarning> Warnings
|
||||
);
|
||||
|
||||
/// <summary>Предупреждение по результату генерации: не ошибка, но админу это видеть нужно.</summary>
|
||||
public sealed record PlanningWarning(PlanningWarningKind Kind, Guid? SlotId, string Details);
|
||||
|
||||
public enum PlanningWarningKind
|
||||
{
|
||||
/// <summary>Слот не дал контента — место закрыл фон.</summary>
|
||||
SlotEmpty = 0,
|
||||
|
||||
/// <summary>Фактический старт ушёл дальше допуска.</summary>
|
||||
DriftExceeded = 1,
|
||||
|
||||
/// <summary>Остывание отсекло всех кандидатов.</summary>
|
||||
CooldownExhausted = 2,
|
||||
|
||||
/// <summary>Не нашлось, что повторить.</summary>
|
||||
RepeatSourceEmpty = 3,
|
||||
|
||||
/// <summary>Пусто даже в фоне — в ленте образуется дыра.</summary>
|
||||
FallbackEmpty = 4,
|
||||
|
||||
/// <summary>Жёсткие фильтры (детское время, потолок повторов) не оставили ни одного кандидата.</summary>
|
||||
CandidatesFiltered = 5,
|
||||
|
||||
// ── Пост-проверки: считаются по готовой ленте и ничего не переигрывают (см. 3.8). ──
|
||||
|
||||
/// <summary>Врезок в часе больше заданного потолка.</summary>
|
||||
BreakLimitExceeded = 6,
|
||||
|
||||
/// <summary>Доля одного жанра за сутки выше заданной.</summary>
|
||||
GenreShareExceeded = 7,
|
||||
|
||||
/// <summary>Фон занял больше эфира, чем считается нормой.</summary>
|
||||
FallbackShareExceeded = 8,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user