Refactor Channel endpoints and data models: replace jingle functionality with bumper templates, update related commands and handlers, and enhance API routes for managing bumper templates. Remove obsolete jingle-related code and adjust channel data structures to support new bumper template features.
This commit is contained in:
@@ -19,18 +19,17 @@ public static class SchedulePlanner
|
||||
var byShowId = input.Shows.ToDictionary(s => s.ShowId);
|
||||
var nextEpisode = input.Shows.ToDictionary(s => s.ChannelShowId, s => s.NextEpisodeIndex);
|
||||
var nextAd = input.NextAdIndex;
|
||||
var nextJingle = input.NextJingleIndex;
|
||||
var nextBumper = input.NextBumperIndex;
|
||||
|
||||
// Есть ли вообще из чего строить эфир.
|
||||
var anyPlayable = input.Shows.Any(s => s.Weight > 0 && s.EpisodeAssetIds.Count > 0);
|
||||
if (!anyPlayable)
|
||||
return new PlannerResult(entries, nextEpisode, nextAd, nextJingle);
|
||||
return new PlannerResult(entries, nextEpisode, nextAd, nextBumper);
|
||||
|
||||
var cursor = input.StartTime;
|
||||
var iterations = 0;
|
||||
Guid? prevShowId = null;
|
||||
DateTimeOffset? lastBumperAt = null;
|
||||
var bumperCount = 0;
|
||||
|
||||
while (cursor < input.HorizonEnd && iterations++ < IterationBackstop)
|
||||
{
|
||||
@@ -40,8 +39,8 @@ public static class SchedulePlanner
|
||||
|
||||
var pick = WeightedPick(candidates, random);
|
||||
|
||||
// ТВ-заставка на переходе. Динамическую (Сейчас/Далее) резервируем слотом фикс. длины —
|
||||
// ассет отрендерит оркестратор; статичный джингл берём готовым из пула (реальная длина).
|
||||
// ТВ-заставка на переходе. Резервируем слот выбранного блока фикс. длины — конкретный
|
||||
// отрендеренный ассет («Сейчас/Далее» стилем блока поверх его звука) подставит оркестратор.
|
||||
if (
|
||||
prevShowId is { } prev
|
||||
&& input.Bumpers is { Enabled: true } bumper
|
||||
@@ -54,11 +53,8 @@ public static class SchedulePlanner
|
||||
)
|
||||
{
|
||||
var bumperStart = cursor;
|
||||
if (TryPlaceBumper(entries, bumper, bumperCount, prev, pick.ShowId, input, ref nextJingle, ref cursor))
|
||||
{
|
||||
if (TryPlaceBumper(entries, bumper, prev, pick.ShowId, random, ref nextBumper, ref cursor))
|
||||
lastBumperAt = bumperStart;
|
||||
bumperCount++;
|
||||
}
|
||||
}
|
||||
|
||||
var blockStart = cursor;
|
||||
@@ -94,75 +90,62 @@ public static class SchedulePlanner
|
||||
prevShowId = pick.ShowId;
|
||||
}
|
||||
|
||||
return new PlannerResult(entries, nextEpisode, nextAd, nextJingle);
|
||||
return new PlannerResult(entries, nextEpisode, nextAd, nextBumper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ставит одну заставку на переходе по режиму канала. Динамическая — плейсхолдер фикс. длины
|
||||
/// (ассет подставит оркестратор). Статичная — готовый джингл из пула (реальная длина, курсор
|
||||
/// двигается). В режиме Both типы чередуются; при пустом пуле Both уходит в динамику.
|
||||
/// Возвращает true, если заставка добавлена (курсор сдвинут).
|
||||
/// Ставит на переходе заставку выбранного блока: резервирует слот его длины и оставляет
|
||||
/// плейсхолдер с парой шоу + id блока (ассет отрендерит оркестратор). Выбор блока — по стратегии
|
||||
/// канала (ротация двигает курсор). Возвращает true, если заставка добавлена (курсор сдвинут).
|
||||
/// </summary>
|
||||
private static bool TryPlaceBumper(
|
||||
List<PlannedEntry> entries,
|
||||
PlannerBumperConfig bumper,
|
||||
int bumperCount,
|
||||
Guid fromShowId,
|
||||
Guid toShowId,
|
||||
PlannerInput input,
|
||||
ref int nextJingle,
|
||||
IRandomSource random,
|
||||
ref int nextBumper,
|
||||
ref DateTimeOffset cursor
|
||||
)
|
||||
{
|
||||
var pool = bumper.JinglePool;
|
||||
var hasPool = pool is { Count: > 0 };
|
||||
|
||||
var wantStatic =
|
||||
bumper.Mode == BumperMode.Static
|
||||
|| (bumper.Mode == BumperMode.Both && bumperCount % 2 == 1);
|
||||
|
||||
// В режиме Both при пустом пуле показываем динамику.
|
||||
if (wantStatic && !hasPool && bumper.Mode == BumperMode.Both)
|
||||
wantStatic = false;
|
||||
|
||||
if (wantStatic)
|
||||
{
|
||||
if (!hasPool)
|
||||
return false; // Static без пула — вставлять нечего.
|
||||
|
||||
var idx = ((nextJingle % pool!.Count) + pool.Count) % pool.Count;
|
||||
var assetId = pool[idx];
|
||||
nextJingle++;
|
||||
var dur = DurationOf(assetId, input);
|
||||
if (dur <= TimeSpan.Zero)
|
||||
return false;
|
||||
|
||||
var end = cursor + dur;
|
||||
entries.Add(
|
||||
new PlannedEntry(assetId, ScheduleEntryKind.Bumper, cursor, end, null, null)
|
||||
);
|
||||
cursor = end;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Динамическая заставка «Сейчас/Далее» — плейсхолдер с парой шоу для рендера.
|
||||
if (bumper.Duration <= TimeSpan.Zero)
|
||||
var templates = bumper.Templates;
|
||||
if (templates is not { Count: > 0 })
|
||||
return false;
|
||||
|
||||
var dynEnd = cursor + bumper.Duration;
|
||||
PlannerBumperTemplate template;
|
||||
switch (bumper.Selection)
|
||||
{
|
||||
case BumperSelection.Random:
|
||||
template = templates[random.Next(templates.Count)];
|
||||
break;
|
||||
case BumperSelection.AlwaysFirst:
|
||||
template = templates[0];
|
||||
break;
|
||||
default: // Rotation
|
||||
var idx = ((nextBumper % templates.Count) + templates.Count) % templates.Count;
|
||||
template = templates[idx];
|
||||
nextBumper++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (template.Duration <= TimeSpan.Zero)
|
||||
return false;
|
||||
|
||||
var end = cursor + template.Duration;
|
||||
entries.Add(
|
||||
new PlannedEntry(
|
||||
Guid.Empty,
|
||||
ScheduleEntryKind.Bumper,
|
||||
cursor,
|
||||
dynEnd,
|
||||
end,
|
||||
toShowId,
|
||||
null,
|
||||
fromShowId,
|
||||
toShowId
|
||||
toShowId,
|
||||
template.TemplateId
|
||||
)
|
||||
);
|
||||
cursor = dynEnd;
|
||||
cursor = end;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,19 +22,22 @@ public sealed record PlannerOverride(
|
||||
public sealed record PlannerOverrideShow(Guid ShowId, int Weight);
|
||||
|
||||
/// <summary>
|
||||
/// Политика ТВ-заставок на переходах. <see cref="Duration"/> должна быть кратна длине сегмента
|
||||
/// (генератор выравнивает). Планировщик резервирует под заставку слот этой длины, а конкретный
|
||||
/// сгенерированный ассет подставляет уже оркестратор.
|
||||
/// Политика ТВ-заставок на переходах. Планировщик выбирает блок (<see cref="Templates"/>) по
|
||||
/// стратегии <see cref="Selection"/> и резервирует слот его длины (<see cref="PlannerBumperTemplate.Duration"/>,
|
||||
/// уже выровнена генератором на сегмент). Конкретный отрендеренный ассет подставляет оркестратор
|
||||
/// по паре шоу + выбранному блоку.
|
||||
/// </summary>
|
||||
public sealed record PlannerBumperConfig(
|
||||
bool Enabled,
|
||||
TimeSpan Duration,
|
||||
bool OnlyBetweenDifferentShows,
|
||||
TimeSpan MinInterval,
|
||||
BumperMode Mode = BumperMode.Dynamic,
|
||||
IReadOnlyList<Guid>? JinglePool = null
|
||||
BumperSelection Selection,
|
||||
IReadOnlyList<PlannerBumperTemplate> Templates
|
||||
);
|
||||
|
||||
/// <summary>Блок заставки в терминах планировщика: id + длительность слота (кратна сегменту).</summary>
|
||||
public sealed record PlannerBumperTemplate(Guid TemplateId, TimeSpan Duration);
|
||||
|
||||
/// <summary>Полный вход планировщика для одного прогона по каналу.</summary>
|
||||
public sealed record PlannerInput(
|
||||
Guid ChannelId,
|
||||
@@ -48,13 +51,14 @@ public sealed record PlannerInput(
|
||||
DateTimeOffset StartTime,
|
||||
DateTimeOffset HorizonEnd,
|
||||
PlannerBumperConfig? Bumpers = null,
|
||||
int NextJingleIndex = 0
|
||||
int NextBumperIndex = 0
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Одна запланированная запись (ещё не доменная сущность). Для заставок (<see cref="Kind"/> ==
|
||||
/// <see cref="ScheduleEntryKind.Bumper"/>) <see cref="MediaAssetId"/> пуст — его подставит
|
||||
/// оркестратор после рендера по паре (<see cref="FromShowId"/> → <see cref="ToShowId"/>).
|
||||
/// оркестратор после рендера по паре (<see cref="FromShowId"/> → <see cref="ToShowId"/>) и выбранному
|
||||
/// блоку (<see cref="BumperTemplateId"/>).
|
||||
/// </summary>
|
||||
public sealed record PlannedEntry(
|
||||
Guid MediaAssetId,
|
||||
@@ -64,13 +68,14 @@ public sealed record PlannedEntry(
|
||||
Guid? ShowId,
|
||||
int? EpisodeIndex,
|
||||
Guid? FromShowId = null,
|
||||
Guid? ToShowId = null
|
||||
Guid? ToShowId = null,
|
||||
Guid? BumperTemplateId = null
|
||||
);
|
||||
|
||||
/// <summary>Результат прогона: новые записи + обновлённые курсоры (серий по каждому ChannelShow и рекламы).</summary>
|
||||
/// <summary>Результат прогона: новые записи + обновлённые курсоры (серий по каждому ChannelShow, рекламы, заставок).</summary>
|
||||
public sealed record PlannerResult(
|
||||
IReadOnlyList<PlannedEntry> Entries,
|
||||
IReadOnlyDictionary<Guid, int> NextEpisodeIndexByChannelShow,
|
||||
int NextAdIndex,
|
||||
int NextJingleIndex
|
||||
int NextBumperIndex
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user