Files
TeleWave/backend/src/TeleWave.Domain/Broadcast/Scheduling/SchedulePlannerModels.cs
T

144 lines
5.9 KiB
C#

namespace TeleWave.Domain.Broadcast.Scheduling;
/// <summary>Шоу канала, подготовленное для планировщика: только готовые серии, с курсором.</summary>
public sealed record PlannerShow(
Guid ChannelShowId,
Guid ShowId,
int Weight,
BlockMode BlockMode,
int BlockValue,
IReadOnlyList<Guid> EpisodeAssetIds,
int NextEpisodeIndex,
IReadOnlyList<PlannerHourWindow>? PreferredHours = null,
int PreferredWeightMultiplier = 1
);
/// <summary>Окно предпочтительных часов [StartHour, EndHour) суток (UTC) для планировщика.</summary>
public sealed record PlannerHourWindow(int StartHour, int EndHour)
{
public bool Contains(int hour) => hour >= StartHour && hour < EndHour;
}
/// <summary>
/// Override в терминах планировщика: режим + шоу с весами + правило действия (разовое окно либо
/// еженедельно по дню недели на окне минут суток UTC).
/// </summary>
public sealed record PlannerOverride(
OverrideMode Mode,
IReadOnlyList<PlannerOverrideShow> Shows,
OverrideRecurrence Recurrence = OverrideRecurrence.OneTime,
DateTimeOffset? StartsAtUtc = null,
DateTimeOffset? EndsAtUtc = null,
int? DayOfWeek = null,
int? StartMinute = null,
int? EndMinute = null
)
{
/// <summary>Действует ли override в этот момент.</summary>
public bool Covers(DateTimeOffset moment)
{
if (Recurrence == OverrideRecurrence.Weekly)
{
if (
DayOfWeek is not { } day
|| StartMinute is not { } start
|| EndMinute is not { } end
)
return false;
var utc = moment.UtcDateTime;
var minuteOfDay = utc.Hour * 60 + utc.Minute;
var today = (int)utc.DayOfWeek;
if (end > start)
// Обычное окно в пределах одних суток [start, end).
return today == day && minuteOfDay >= start && minuteOfDay < end;
if (end < start)
{
// Окно пересекает полночь: [start, 24:00) в день `day` и [00:00, end) на следующий день.
var nextDay = (day + 1) % 7;
return (today == day && minuteOfDay >= start)
|| (today == nextDay && minuteOfDay < end);
}
return false; // end == start — пустое окно
}
return moment >= StartsAtUtc && moment < EndsAtUtc;
}
}
public sealed record PlannerOverrideShow(Guid ShowId, int Weight);
/// <summary>
/// Политика ТВ-заставок на переходах. Планировщик из подходящих подблоков (<see cref="Variants"/>,
/// фильтр по <see cref="PlannerBumperVariant.Trigger"/> и контексту перехода) выбирает один по стратегии
/// <see cref="Selection"/> и резервирует слот длины его блока. Ассет подставляет оркестратор.
/// <see cref="ShowChangeChance"/>/<see cref="EpisodeChangeChance"/> — вероятность самого появления
/// заставки на смене шоу / между блоками одного шоу (0..1).
/// </summary>
public sealed record PlannerBumperConfig(
bool Enabled,
TimeSpan MinInterval,
BumperSelection Selection,
IReadOnlyList<PlannerBumperVariant> Variants,
double ShowChangeChance = 1.0,
double EpisodeChangeChance = 1.0
);
/// <summary>
/// Подблок заставки в терминах планировщика: id варианта + id родительского блока (стиль/звук) +
/// длительность слота (кратна сегменту) + правило показа + вес (для <see cref="BumperSelection.WeightedRandom"/>).
/// </summary>
public sealed record PlannerBumperVariant(
Guid VariantId,
Guid TemplateId,
TimeSpan Duration,
BumperTrigger Trigger,
int Weight = 1
);
/// <summary>Полный вход планировщика для одного прогона по каналу.</summary>
public sealed record PlannerInput(
Guid ChannelId,
AdInsertion AdInsertion,
int AdsPerBreak,
int NextAdIndex,
IReadOnlyList<PlannerShow> Shows,
IReadOnlyList<Guid> AdPool,
IReadOnlyDictionary<Guid, TimeSpan> Durations,
IReadOnlyList<PlannerOverride> Overrides,
DateTimeOffset StartTime,
DateTimeOffset HorizonEnd,
PlannerBumperConfig? Bumpers = null,
int NextBumperIndex = 0
);
/// <summary>
/// Одна запланированная запись (ещё не доменная сущность). Для заставок (<see cref="Kind"/> ==
/// <see cref="ScheduleEntryKind.Bumper"/>) <see cref="MediaAssetId"/> пуст — его подставит
/// оркестратор после рендера по паре (<see cref="FromShowId"/> → <see cref="ToShowId"/>) и выбранному
/// блоку (<see cref="BumperTemplateId"/>).
/// </summary>
public sealed record PlannedEntry(
Guid MediaAssetId,
ScheduleEntryKind Kind,
DateTimeOffset StartsAtUtc,
DateTimeOffset EndsAtUtc,
Guid? ShowId,
int? EpisodeIndex,
Guid? FromShowId = null,
Guid? ToShowId = null,
Guid? BumperTemplateId = null,
Guid? BumperVariantId = null
);
/// <summary>Результат прогона: новые записи + обновлённые курсоры (серий по каждому ChannelShow, рекламы, заставок).</summary>
public sealed record PlannerResult(
IReadOnlyList<PlannedEntry> Entries,
IReadOnlyDictionary<Guid, int> NextEpisodeIndexByChannelShow,
int NextAdIndex,
int NextBumperIndex
);