144 lines
5.9 KiB
C#
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
|
|
);
|