Add bumper variant management: implement API endpoints for adding, updating, and removing bumper text variants, enhance data models to support variant details, and update scheduling logic to utilize variants. Refactor related components for improved bumper template handling and ensure proper error management for variant operations.

This commit is contained in:
Leonid Pershin
2026-07-25 13:24:11 +03:00
parent f640af1fc4
commit a65bcf4258
34 changed files with 2009 additions and 170 deletions
@@ -53,6 +53,7 @@ public sealed class ScheduleGenerator(
.Include(c => c.Shows)
.Include(c => c.Ads)
.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants)
.Include(c => c.Overrides)
.ThenInclude(o => o.Shows)
.AsSplitQuery()
@@ -147,16 +148,16 @@ public sealed class ScheduleGenerator(
private static ScheduleEntry? BuildBumperEntry(
Guid channelId,
PlannedEntry entry,
IReadOnlyDictionary<(Guid From, Guid To, Guid Template), Guid> bumperAssets
IReadOnlyDictionary<(Guid From, Guid To, Guid Variant), Guid> bumperAssets
)
{
// Заставка резолвится по паре шоу + выбранному блоку (отрендерена/из кэша). Если рендер не
// Заставка резолвится по паре шоу + выбранному подблоку (отрендерена/из кэша). Если рендер не
// удался — пропускаем запись (слот заполнит филлер/следующая программа; длину планировщик учёл).
if (
entry.FromShowId is not { } from
|| entry.ToShowId is not { } to
|| entry.BumperTemplateId is not { } template
|| !bumperAssets.TryGetValue((from, to, template), out var assetId)
|| entry.BumperVariantId is not { } variant
|| !bumperAssets.TryGetValue((from, to, variant), out var assetId)
)
return null;
@@ -164,10 +165,10 @@ public sealed class ScheduleGenerator(
}
/// <summary>
/// Для каждой уникальной тройки «из→в→блок» из запланированных заставок возвращает id готового
/// Для каждой уникальной тройки «из→в→подблок» из запланированных заставок возвращает id готового
/// ассета-заставки: из кэша (<see cref="BumperAsset"/>) либо свежесгенерированного.
/// </summary>
private async Task<Dictionary<(Guid From, Guid To, Guid Template), Guid>> ResolveBumperAssetsAsync(
private async Task<Dictionary<(Guid From, Guid To, Guid Variant), Guid>> ResolveBumperAssetsAsync(
Channel channel,
IReadOnlyList<PlannedEntry> entries,
IReadOnlyDictionary<Guid, string> showNames,
@@ -176,17 +177,20 @@ public sealed class ScheduleGenerator(
{
var result = new Dictionary<(Guid, Guid, Guid), Guid>();
var templatesById = channel.BumperTemplates.ToDictionary(t => t.Id);
var variantsById = channel.BumperTemplates
.SelectMany(t => t.Variants.Select(v => (Variant: v, Template: t)))
.ToDictionary(x => x.Variant.Id);
var combos = entries
.Where(e =>
e.Kind == ScheduleEntryKind.Bumper
&& e.FromShowId is not null
&& e.ToShowId is not null
&& e.BumperTemplateId is not null
&& e.BumperVariantId is not null
)
.Select(e => (
From: e.FromShowId!.Value,
To: e.ToShowId!.Value,
Template: e.BumperTemplateId!.Value
Variant: e.BumperVariantId!.Value
))
.Distinct()
.ToList();
@@ -256,17 +260,20 @@ public sealed class ScheduleGenerator(
foreach (var combo in combos)
{
if (!templatesById.TryGetValue(combo.Template, out var template))
if (!variantsById.TryGetValue(combo.Variant, out var pair))
continue;
var (variant, template) = pair;
var fromName = showNames.GetValueOrDefault(combo.From, "…");
var toName = showNames.GetValueOrDefault(combo.To, "…");
var poster = posterByShow.TryGetValue(combo.To, out var pr) ? pr : default;
// Постер шоу-получателя как фон — только для «Сейчас/Далее» (свободный текст шоу не упоминает).
var usePoster = variant.Kind == BumperTextKind.NowNext;
var poster = usePoster && posterByShow.TryGetValue(combo.To, out var pr) ? pr : default;
var posterToken = poster.ImageId == Guid.Empty ? "-" : poster.ImageId.ToString();
var posterAbs = poster.ImageId == Guid.Empty ? null : poster.AbsPath;
var bgAbs = bgByTemplate.GetValueOrDefault(template.Id);
var aligned = AlignedDurationSeconds(TemplateDurationSeconds(template));
var signature = ComputeSignature(channel, template, fromName, toName, aligned, posterToken);
var signature = ComputeSignature(channel, template, variant, fromName, toName, aligned, posterToken);
var hit = cached.FirstOrDefault(c =>
c.FromShowId == combo.From
@@ -285,6 +292,7 @@ public sealed class ScheduleGenerator(
var assetId = await RenderBumperAsync(
channel,
template,
variant,
combo.From,
combo.To,
fromName,
@@ -314,6 +322,7 @@ public sealed class ScheduleGenerator(
private async Task<Guid> RenderBumperAsync(
Channel channel,
BumperTemplate template,
BumperTextVariant variant,
Guid fromShowId,
Guid toShowId,
string fromName,
@@ -331,6 +340,7 @@ public sealed class ScheduleGenerator(
BuildSpec(
channel,
template,
variant,
alignedDurationSeconds,
fromName,
toName,
@@ -361,13 +371,16 @@ public sealed class ScheduleGenerator(
private BumperRenderSpec BuildSpec(
Channel channel,
BumperTemplate template,
BumperTextVariant variant,
int alignedDurationSeconds,
string fromName,
string toName,
string? posterAbsolutePath,
string? backgroundAbsolutePath
) =>
new(
)
{
var free = variant.Kind == BumperTextKind.Free;
return new BumperRenderSpec(
alignedDurationSeconds,
_bumper.Width,
_bumper.Height,
@@ -376,14 +389,18 @@ public sealed class ScheduleGenerator(
template.AccentColor,
template.TextColor,
FontPath(channel.BumperFont),
channel.BumperNowLabel,
fromName,
channel.BumperNextLabel,
toName,
free ? "" : variant.NowLabel,
free ? "" : fromName,
free ? "" : variant.NextLabel,
free ? "" : toName,
backgroundAbsolutePath,
bumperStorage.AudioPath(template.Id, template.AudioExtension),
posterAbsolutePath
posterAbsolutePath,
free,
variant.Line1,
variant.Line2
);
}
private string FontPath(BumperFont font) =>
font == BumperFont.Serif ? _bumper.FontFileSerif : _bumper.FontFileSans;
@@ -407,6 +424,7 @@ public sealed class ScheduleGenerator(
private string ComputeSignature(
Channel channel,
BumperTemplate template,
BumperTextVariant variant,
string fromName,
string toName,
int alignedDurationSeconds,
@@ -420,8 +438,11 @@ public sealed class ScheduleGenerator(
_bumper.Height,
alignedDurationSeconds,
channel.BumperFont,
channel.BumperNowLabel,
channel.BumperNextLabel,
variant.Kind,
variant.NowLabel,
variant.NextLabel,
variant.Line1,
variant.Line2,
template.BackgroundColor,
template.BackgroundColor2,
template.AccentColor,
@@ -515,13 +536,16 @@ public sealed class ScheduleGenerator(
.Where(durations.ContainsKey)
.ToList();
// Блоки заставок: длительность слота — по звуку (или дефолт), выровнена на сегмент.
var bumperTemplates = channel.BumperTemplates
// Подблоки заставок (плоский список): длительность слота — по звуку блока, выровнена на сегмент.
var bumperVariants = channel.BumperTemplates
.OrderBy(t => t.Position)
.Select(t => new PlannerBumperTemplate(
t.Id,
TimeSpan.FromSeconds(AlignedDurationSeconds(TemplateDurationSeconds(t)))
))
.SelectMany(t =>
{
var dur = TimeSpan.FromSeconds(AlignedDurationSeconds(TemplateDurationSeconds(t)));
return t.Variants
.OrderBy(v => v.Position)
.Select(v => new PlannerBumperVariant(v.Id, t.Id, dur, v.Trigger));
})
.ToList();
var overrides = channel.Overrides
@@ -535,10 +559,9 @@ public sealed class ScheduleGenerator(
var bumpers = new PlannerBumperConfig(
channel.BumpersEnabled,
channel.BumperOnlyBetweenDifferentShows,
TimeSpan.FromMinutes(channel.BumperMinIntervalMinutes),
channel.BumperSelection,
bumperTemplates
bumperVariants
);
return new PlannerInput(