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:
@@ -1,14 +0,0 @@
|
||||
namespace TeleWave.Domain.Broadcast;
|
||||
|
||||
/// <summary>Какие заставки вставлять на переходах.</summary>
|
||||
public enum BumperMode
|
||||
{
|
||||
/// <summary>Только динамические «Сейчас/Далее», отрисованные по оформлению канала.</summary>
|
||||
Dynamic,
|
||||
|
||||
/// <summary>Только готовые ролики-джинглы из пула канала (по кругу).</summary>
|
||||
Static,
|
||||
|
||||
/// <summary>И то, и другое — чередуя на соседних переходах.</summary>
|
||||
Both,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace TeleWave.Domain.Broadcast;
|
||||
|
||||
/// <summary>Как выбирать блок заставки на каждом переходе между шоу.</summary>
|
||||
public enum BumperSelection
|
||||
{
|
||||
/// <summary>По кругу в порядке блоков (курсор <see cref="Channel.NextBumperIndex"/>).</summary>
|
||||
Rotation,
|
||||
|
||||
/// <summary>Случайный блок на каждом переходе.</summary>
|
||||
Random,
|
||||
|
||||
/// <summary>Всегда первый (дефолтный) блок.</summary>
|
||||
AlwaysFirst,
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
namespace TeleWave.Domain.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Блок ТВ-заставки канала: свой звук + своё оформление (цвета, опциональная фон-картинка). На
|
||||
/// переходе между шоу генератор рендерит «Сейчас/Далее» стилем блока поверх его звука; длительность
|
||||
/// заставки определяется длиной звука (выравнивается на сегмент при рендере). Общие для канала шрифт,
|
||||
/// подписи и правила показа живут на <see cref="Channel"/>.
|
||||
///
|
||||
/// Первый блок (<see cref="Position"/> == 0) — дефолтный, не удаляется; если звук в нём не загружен,
|
||||
/// рендер синтезирует джингл по умолчанию.
|
||||
/// </summary>
|
||||
public class BumperTemplate
|
||||
{
|
||||
public Guid Id { get; private set; }
|
||||
public Guid ChannelId { get; private set; }
|
||||
|
||||
/// <summary>Порядковый номер (0 — дефолтный блок). Используется ротацией и как признак дефолта.</summary>
|
||||
public int Position { get; private set; }
|
||||
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
// ── Оформление блока (цвета — в нотации ffmpeg: 0xRRGGBB или имя) ──
|
||||
public string BackgroundColor { get; private set; } = DefaultBackgroundColor;
|
||||
public string BackgroundColor2 { get; private set; } = DefaultBackgroundColor2;
|
||||
public string AccentColor { get; private set; } = DefaultAccentColor;
|
||||
public string TextColor { get; private set; } = DefaultTextColor;
|
||||
|
||||
/// <summary>Расширение загруженной фон-картинки (с точкой) или null — тогда фон градиент/постер.</summary>
|
||||
public string? BackgroundImageExtension { get; private set; }
|
||||
|
||||
/// <summary>Расширение загруженного звука (с точкой) или null — тогда синтезируется джингл.</summary>
|
||||
public string? AudioExtension { get; private set; }
|
||||
|
||||
/// <summary>Длина загруженного звука в секундах (замер ffprobe) или null, если звука нет.</summary>
|
||||
public double? AudioDurationSeconds { get; private set; }
|
||||
|
||||
/// <summary>Версия файлов блока (звук/фон). Входит в кэш-ключ рендера — замена файла пересобирает заставки.</summary>
|
||||
public int Revision { get; private set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
public const string DefaultBackgroundColor = "0x0b1020";
|
||||
public const string DefaultBackgroundColor2 = "0x1e293b";
|
||||
public const string DefaultAccentColor = "0x38bdf8";
|
||||
public const string DefaultTextColor = "white";
|
||||
|
||||
public bool IsDefault => Position == 0;
|
||||
|
||||
private BumperTemplate() { }
|
||||
|
||||
internal static BumperTemplate Create(Guid channelId, int position, string name) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ChannelId = channelId,
|
||||
Position = position,
|
||||
Name = name,
|
||||
BackgroundColor = DefaultBackgroundColor,
|
||||
BackgroundColor2 = DefaultBackgroundColor2,
|
||||
AccentColor = DefaultAccentColor,
|
||||
TextColor = DefaultTextColor,
|
||||
BackgroundImageExtension = null,
|
||||
AudioExtension = null,
|
||||
AudioDurationSeconds = null,
|
||||
Revision = 0,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
/// <summary>Обновить имя и цвета блока. Цвета — в нотации ffmpeg (0xRRGGBB или имя).</summary>
|
||||
public void UpdateStyle(
|
||||
string name,
|
||||
string backgroundColor,
|
||||
string backgroundColor2,
|
||||
string accentColor,
|
||||
string textColor
|
||||
)
|
||||
{
|
||||
Name = name;
|
||||
BackgroundColor = backgroundColor;
|
||||
BackgroundColor2 = backgroundColor2;
|
||||
AccentColor = accentColor;
|
||||
TextColor = textColor;
|
||||
}
|
||||
|
||||
/// <summary>Отметить загруженный звук (extension — с точкой, нижний регистр) и его длину. Меняет ревизию.</summary>
|
||||
public void SetAudio(string extension, double durationSeconds)
|
||||
{
|
||||
AudioExtension = extension;
|
||||
AudioDurationSeconds = durationSeconds > 0 ? durationSeconds : null;
|
||||
Revision++;
|
||||
}
|
||||
|
||||
public void ClearAudio()
|
||||
{
|
||||
if (AudioExtension is null && AudioDurationSeconds is null)
|
||||
return;
|
||||
AudioExtension = null;
|
||||
AudioDurationSeconds = null;
|
||||
Revision++;
|
||||
}
|
||||
|
||||
/// <summary>Отметить загруженную фон-картинку (extension — с точкой, нижний регистр). Меняет ревизию.</summary>
|
||||
public void SetBackgroundImage(string extension)
|
||||
{
|
||||
BackgroundImageExtension = extension;
|
||||
Revision++;
|
||||
}
|
||||
|
||||
public void ClearBackgroundImage()
|
||||
{
|
||||
if (BackgroundImageExtension is null)
|
||||
return;
|
||||
BackgroundImageExtension = null;
|
||||
Revision++;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public class Channel
|
||||
private readonly List<ChannelShow> _shows = new();
|
||||
private readonly List<ChannelAd> _ads = new();
|
||||
private readonly List<ProgrammingOverride> _overrides = new();
|
||||
private readonly List<ChannelJingle> _jingles = new();
|
||||
private readonly List<BumperTemplate> _bumperTemplates = new();
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
@@ -26,28 +26,14 @@ public class Channel
|
||||
/// <summary>Вставлять ли ТВ-заставки на переходах между разными шоу.</summary>
|
||||
public bool BumpersEnabled { get; private set; }
|
||||
|
||||
/// <summary>Какие заставки вставлять: динамические «Сейчас/Далее», статичные джинглы или оба.</summary>
|
||||
public BumperMode BumperMode { get; private set; }
|
||||
// ── Общие для канала настройки ТВ-заставок (стиль/звук — на каждом блоке, см. BumperTemplates) ──
|
||||
|
||||
/// <summary>Расширение загруженного фона (с точкой) или null — тогда синтезируется градиент.</summary>
|
||||
public string? BumperBackgroundExtension { get; private set; }
|
||||
/// <summary>Как выбирать блок заставки на каждом переходе (по кругу/случайно/всегда первый).</summary>
|
||||
public BumperSelection BumperSelection { get; private set; }
|
||||
|
||||
/// <summary>Расширение загруженной музыки (с точкой) или null — тогда синтезируется джингл.</summary>
|
||||
public string? BumperMusicExtension { get; private set; }
|
||||
/// <summary>Курсор ротации блоков заставок.</summary>
|
||||
public int NextBumperIndex { get; private set; }
|
||||
|
||||
/// <summary>Счётчик версии файлов заставки (фон/музыка). Входит в кэш-ключ, чтобы замена файла тем
|
||||
/// же именем пересобирала уже отрендеренные динамические заставки.</summary>
|
||||
public int BumperRevision { get; private set; }
|
||||
|
||||
/// <summary>Курсор ротации пула джинглов.</summary>
|
||||
public int NextJingleIndex { get; private set; }
|
||||
|
||||
// ── Оформление и правила ТВ-заставок (значения на канал; см. UpdateBumperSettings) ──
|
||||
public int BumperDurationSeconds { get; private set; }
|
||||
public string BumperBackgroundColor { get; private set; } = DefaultBackgroundColor;
|
||||
public string BumperBackgroundColor2 { get; private set; } = DefaultBackgroundColor2;
|
||||
public string BumperAccentColor { get; private set; } = DefaultAccentColor;
|
||||
public string BumperTextColor { get; private set; } = DefaultTextColor;
|
||||
public BumperFont BumperFont { get; private set; }
|
||||
public string BumperNowLabel { get; private set; } = DefaultNowLabel;
|
||||
public string BumperNextLabel { get; private set; } = DefaultNextLabel;
|
||||
@@ -58,13 +44,9 @@ public class Channel
|
||||
/// <summary>Ставить заставку только на смене шоу (иначе — и внутри марафона одного шоу).</summary>
|
||||
public bool BumperOnlyBetweenDifferentShows { get; private set; }
|
||||
|
||||
private const int DefaultBumperDurationSeconds = 8;
|
||||
private const string DefaultBackgroundColor = "0x0b1020";
|
||||
private const string DefaultBackgroundColor2 = "0x1e293b";
|
||||
private const string DefaultAccentColor = "0x38bdf8";
|
||||
private const string DefaultTextColor = "white";
|
||||
private const string DefaultNowLabel = "СЕЙЧАС";
|
||||
private const string DefaultNextLabel = "ДАЛЕЕ";
|
||||
private const string DefaultTemplateName = "Заставка 1";
|
||||
|
||||
/// <summary>Ассет-заглушка на случай пустого расписания (аварийная подстраховка).</summary>
|
||||
public Guid? FillerAssetId { get; private set; }
|
||||
@@ -80,13 +62,14 @@ public class Channel
|
||||
public IReadOnlyList<ChannelAd> Ads => _ads;
|
||||
public IReadOnlyList<ProgrammingOverride> Overrides => _overrides;
|
||||
|
||||
/// <summary>Пул джинглов-отбивок; порядок ротации — по <see cref="ChannelJingle.Position"/>.</summary>
|
||||
public IReadOnlyList<ChannelJingle> Jingles => _jingles;
|
||||
/// <summary>Блоки заставок (звук+стиль); первый (Position 0) — дефолтный, порядок — по Position.</summary>
|
||||
public IReadOnlyList<BumperTemplate> BumperTemplates => _bumperTemplates;
|
||||
|
||||
private Channel() { }
|
||||
|
||||
public static Channel Create(string name, string slug, DateTimeOffset epochUtc) =>
|
||||
new()
|
||||
public static Channel Create(string name, string slug, DateTimeOffset epochUtc)
|
||||
{
|
||||
var channel = new Channel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
@@ -96,16 +79,8 @@ public class Channel
|
||||
AdInsertion = AdInsertion.BetweenBlocks,
|
||||
AdsPerBreak = 1,
|
||||
BumpersEnabled = false,
|
||||
BumperMode = BumperMode.Dynamic,
|
||||
BumperBackgroundExtension = null,
|
||||
BumperMusicExtension = null,
|
||||
BumperRevision = 0,
|
||||
NextJingleIndex = 0,
|
||||
BumperDurationSeconds = DefaultBumperDurationSeconds,
|
||||
BumperBackgroundColor = DefaultBackgroundColor,
|
||||
BumperBackgroundColor2 = DefaultBackgroundColor2,
|
||||
BumperAccentColor = DefaultAccentColor,
|
||||
BumperTextColor = DefaultTextColor,
|
||||
BumperSelection = BumperSelection.Rotation,
|
||||
NextBumperIndex = 0,
|
||||
BumperFont = BumperFont.Sans,
|
||||
BumperNowLabel = DefaultNowLabel,
|
||||
BumperNextLabel = DefaultNextLabel,
|
||||
@@ -114,6 +89,10 @@ public class Channel
|
||||
NextAdIndex = 0,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
// На канале всегда есть дефолтный блок заставки (без звука → синтезированный джингл).
|
||||
channel._bumperTemplates.Add(BumperTemplate.Create(channel.Id, 0, DefaultTemplateName));
|
||||
return channel;
|
||||
}
|
||||
|
||||
public void UpdateSettings(
|
||||
string name,
|
||||
@@ -132,85 +111,48 @@ public class Channel
|
||||
FillerAssetId = fillerAssetId;
|
||||
}
|
||||
|
||||
/// <summary>Оформление и правила ТВ-заставок канала. Цвета — в нотации ffmpeg (0xRRGGBB или имя).</summary>
|
||||
/// <summary>Общие настройки ТВ-заставок канала: шрифт, подписи, правила показа и стратегия выбора блока.</summary>
|
||||
public void UpdateBumperSettings(
|
||||
BumperMode mode,
|
||||
int durationSeconds,
|
||||
string backgroundColor,
|
||||
string backgroundColor2,
|
||||
string accentColor,
|
||||
string textColor,
|
||||
BumperFont font,
|
||||
string nowLabel,
|
||||
string nextLabel,
|
||||
int minIntervalMinutes,
|
||||
bool onlyBetweenDifferentShows
|
||||
bool onlyBetweenDifferentShows,
|
||||
BumperSelection selection
|
||||
)
|
||||
{
|
||||
BumperMode = mode;
|
||||
BumperDurationSeconds = durationSeconds;
|
||||
BumperBackgroundColor = backgroundColor;
|
||||
BumperBackgroundColor2 = backgroundColor2;
|
||||
BumperAccentColor = accentColor;
|
||||
BumperTextColor = textColor;
|
||||
BumperFont = font;
|
||||
BumperNowLabel = nowLabel;
|
||||
BumperNextLabel = nextLabel;
|
||||
BumperMinIntervalMinutes = Math.Max(0, minIntervalMinutes);
|
||||
BumperOnlyBetweenDifferentShows = onlyBetweenDifferentShows;
|
||||
BumperSelection = selection;
|
||||
}
|
||||
|
||||
/// <summary>Отметить загруженный фон (extension — с точкой, нижний регистр). Меняет ревизию.</summary>
|
||||
public void SetBumperBackground(string extension)
|
||||
/// <summary>Добавить блок заставки в конец списка. Возвращает созданный блок.</summary>
|
||||
public BumperTemplate AddBumperTemplate(string name)
|
||||
{
|
||||
BumperBackgroundExtension = extension;
|
||||
BumperRevision++;
|
||||
var nextPosition = _bumperTemplates.Count == 0 ? 0 : _bumperTemplates.Max(t => t.Position) + 1;
|
||||
var template = BumperTemplate.Create(Id, nextPosition, name);
|
||||
_bumperTemplates.Add(template);
|
||||
return template;
|
||||
}
|
||||
|
||||
public void ClearBumperBackground()
|
||||
{
|
||||
if (BumperBackgroundExtension is null)
|
||||
return;
|
||||
BumperBackgroundExtension = null;
|
||||
BumperRevision++;
|
||||
}
|
||||
public BumperTemplate? FindBumperTemplate(Guid templateId) =>
|
||||
_bumperTemplates.FirstOrDefault(t => t.Id == templateId);
|
||||
|
||||
/// <summary>Отметить загруженную музыку (extension — с точкой, нижний регистр). Меняет ревизию.</summary>
|
||||
public void SetBumperMusic(string extension)
|
||||
/// <summary>Удалить блок заставки. Дефолтный (Position 0) удалить нельзя — вернёт false.</summary>
|
||||
public bool RemoveBumperTemplate(Guid templateId)
|
||||
{
|
||||
BumperMusicExtension = extension;
|
||||
BumperRevision++;
|
||||
}
|
||||
|
||||
public void ClearBumperMusic()
|
||||
{
|
||||
if (BumperMusicExtension is null)
|
||||
return;
|
||||
BumperMusicExtension = null;
|
||||
BumperRevision++;
|
||||
}
|
||||
|
||||
public ChannelJingle AddJingle(Guid mediaAssetId)
|
||||
{
|
||||
var nextPosition = _jingles.Count == 0 ? 0 : _jingles.Max(j => j.Position) + 1;
|
||||
var jingle = ChannelJingle.Create(Id, mediaAssetId, nextPosition);
|
||||
_jingles.Add(jingle);
|
||||
return jingle;
|
||||
}
|
||||
|
||||
public bool RemoveJingle(Guid channelJingleId)
|
||||
{
|
||||
var jingle = _jingles.FirstOrDefault(j => j.Id == channelJingleId);
|
||||
if (jingle is null)
|
||||
var template = _bumperTemplates.FirstOrDefault(t => t.Id == templateId);
|
||||
if (template is null || template.IsDefault)
|
||||
return false;
|
||||
_jingles.Remove(jingle);
|
||||
_bumperTemplates.Remove(template);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool HasJingle(Guid mediaAssetId) => _jingles.Any(j => j.MediaAssetId == mediaAssetId);
|
||||
|
||||
/// <summary>Планировщик двигает курсор пула джинглов по мере вставки отбивок.</summary>
|
||||
public void SetNextJingleIndex(int index) => NextJingleIndex = index;
|
||||
/// <summary>Планировщик двигает курсор ротации блоков заставок по мере вставки.</summary>
|
||||
public void SetNextBumperIndex(int index) => NextBumperIndex = index;
|
||||
|
||||
public ChannelShow? FindShow(Guid channelShowId) => _shows.FirstOrDefault(s => s.Id == channelShowId);
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace TeleWave.Domain.Broadcast;
|
||||
|
||||
/// <summary>Готовый ролик-джингл (отбивка) в пуле канала. Крутятся по кругу в порядке <see cref="Position"/>
|
||||
/// на переходах между шоу, когда режим заставок — Static или Both.</summary>
|
||||
public class ChannelJingle
|
||||
{
|
||||
public Guid Id { get; private set; }
|
||||
public Guid ChannelId { get; private set; }
|
||||
public Guid MediaAssetId { get; private set; }
|
||||
public int Position { get; private set; }
|
||||
|
||||
private ChannelJingle() { }
|
||||
|
||||
internal static ChannelJingle Create(Guid channelId, Guid mediaAssetId, int position) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ChannelId = channelId,
|
||||
MediaAssetId = mediaAssetId,
|
||||
Position = position,
|
||||
};
|
||||
}
|
||||
@@ -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