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:
Leonid Pershin
2026-07-25 11:02:16 +03:00
parent 27571a4ab6
commit 84c2867062
60 changed files with 2805 additions and 1045 deletions
@@ -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;
}