Update scheduling parameters and refactor channel endpoints: extend HorizonDays to 7 and RetentionDays to 90 in appsettings.json. Consolidate channel-related endpoint logic by removing obsolete files and enhancing the ShowEndpoints with audience and genre management capabilities. Improve error handling and streamline command handlers for channel operations.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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>
|
||||
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
|
||||
)
|
||||
{
|
||||
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
|
||||
);
|
||||
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user