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
@@ -20,6 +20,26 @@ public class SchedulePlannerTests
private static Dictionary<Guid, TimeSpan> Durations(params (Guid Id, int Minutes)[] items) =>
items.ToDictionary(x => x.Id, x => TimeSpan.FromMinutes(x.Minutes));
private static readonly Guid DefaultTemplate = Guid.NewGuid();
/// <summary>Конфиг заставок с одним дефолтным блоком (8с), если явно не заданы блоки.</summary>
private static PlannerBumperConfig Bumper(
bool enabled,
bool onlyBetweenDifferentShows,
TimeSpan minInterval,
BumperSelection selection = BumperSelection.Rotation,
params PlannerBumperTemplate[] templates
) =>
new(
enabled,
onlyBetweenDifferentShows,
minInterval,
selection,
templates.Length == 0
? [new PlannerBumperTemplate(DefaultTemplate, TimeSpan.FromSeconds(8))]
: templates
);
[Fact]
public void Count_Block_ProducesConsecutiveEpisodes_BackToBack()
{
@@ -212,7 +232,7 @@ public class SchedulePlannerTests
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
{
Bumpers = new PlannerBumperConfig(true, TimeSpan.FromSeconds(8), OnlyBetweenDifferentShows: true, MinInterval: TimeSpan.Zero),
Bumpers = Bumper(true, onlyBetweenDifferentShows: true, TimeSpan.Zero),
};
// Чередуем выбор: roll 0 → a, roll 1 → b (веса 1/1, total 2).
@@ -228,6 +248,7 @@ public class SchedulePlannerTests
Assert.Equal(Guid.Empty, first.MediaAssetId); // ассет подставит оркестратор
Assert.Equal(a.ShowId, first.FromShowId);
Assert.Equal(b.ShowId, first.ToShowId);
Assert.Equal(DefaultTemplate, first.BumperTemplateId);
Assert.Equal(TimeSpan.FromSeconds(8), first.EndsAtUtc - first.StartsAtUtc);
// Встык: программа → заставка → программа.
Assert.Equal(result.Entries[0].EndsAtUtc, first.StartsAtUtc);
@@ -246,7 +267,7 @@ public class SchedulePlannerTests
horizonEnd: Start.AddMinutes(50)
) with
{
Bumpers = new PlannerBumperConfig(true, TimeSpan.FromSeconds(8), OnlyBetweenDifferentShows: true, MinInterval: TimeSpan.Zero),
Bumpers = Bumper(true, onlyBetweenDifferentShows: true, TimeSpan.Zero),
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
@@ -266,7 +287,7 @@ public class SchedulePlannerTests
horizonEnd: Start.AddMinutes(50)
) with
{
Bumpers = new PlannerBumperConfig(true, TimeSpan.FromSeconds(8), OnlyBetweenDifferentShows: false, MinInterval: TimeSpan.Zero),
Bumpers = Bumper(true, onlyBetweenDifferentShows: false, TimeSpan.Zero),
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
@@ -284,12 +305,7 @@ public class SchedulePlannerTests
// Два перехода в горизонте (~на 20-й и ~40-й минуте), но интервал 30 мин пропускает второй.
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
{
Bumpers = new PlannerBumperConfig(
true,
TimeSpan.FromSeconds(8),
OnlyBetweenDifferentShows: true,
MinInterval: TimeSpan.FromMinutes(30)
),
Bumpers = Bumper(true, onlyBetweenDifferentShows: true, TimeSpan.FromMinutes(30)),
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));
@@ -298,37 +314,70 @@ public class SchedulePlannerTests
}
[Fact]
public void Bumpers_StaticMode_UsesJinglesFromPoolInRotation()
public void Bumpers_Rotation_CyclesTemplatesAndAdvancesCursor()
{
var a = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
var b = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
Guid j0 = Guid.NewGuid(),
j1 = Guid.NewGuid();
var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20), (j0, 1), (j1, 1));
var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20));
Guid t0 = Guid.NewGuid(),
t1 = Guid.NewGuid();
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
var input = BaseInput([a, b], durations, Start.AddMinutes(80)) with
{
Bumpers = new PlannerBumperConfig(
Bumpers = Bumper(
true,
TimeSpan.FromSeconds(8),
OnlyBetweenDifferentShows: true,
MinInterval: TimeSpan.Zero,
Mode: BumperMode.Static,
JinglePool: [j0, j1]
onlyBetweenDifferentShows: true,
TimeSpan.Zero,
BumperSelection.Rotation,
new PlannerBumperTemplate(t0, TimeSpan.FromSeconds(8)),
new PlannerBumperTemplate(t1, TimeSpan.FromSeconds(4))
),
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));
var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList();
Assert.Equal(2, bumpers.Count);
Assert.Equal([j0, j1], bumpers.Select(e => e.MediaAssetId)); // ротация пула
Assert.All(bumpers, e => Assert.Null(e.FromShowId)); // статик — не по паре шоу
Assert.Equal(2, result.NextJingleIndex);
Assert.True(bumpers.Count >= 2);
// Ротация: первый блок → t0 (8с), второй → t1 (4с). Все — плейсхолдеры по паре шоу.
Assert.Equal(t0, bumpers[0].BumperTemplateId);
Assert.Equal(TimeSpan.FromSeconds(8), bumpers[0].EndsAtUtc - bumpers[0].StartsAtUtc);
Assert.Equal(t1, bumpers[1].BumperTemplateId);
Assert.Equal(TimeSpan.FromSeconds(4), bumpers[1].EndsAtUtc - bumpers[1].StartsAtUtc);
Assert.All(bumpers, e => Assert.Equal(Guid.Empty, e.MediaAssetId));
Assert.Equal(bumpers.Count, result.NextBumperIndex);
}
[Fact]
public void Bumpers_StaticMode_EmptyPool_NoBumpers()
public void Bumpers_AlwaysFirst_AlwaysUsesFirstTemplate()
{
var a = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
var b = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20));
Guid t0 = Guid.NewGuid(),
t1 = Guid.NewGuid();
var input = BaseInput([a, b], durations, Start.AddMinutes(80)) with
{
Bumpers = Bumper(
true,
onlyBetweenDifferentShows: true,
TimeSpan.Zero,
BumperSelection.AlwaysFirst,
new PlannerBumperTemplate(t0, TimeSpan.FromSeconds(8)),
new PlannerBumperTemplate(t1, TimeSpan.FromSeconds(4))
),
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));
var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList();
Assert.True(bumpers.Count >= 2);
Assert.All(bumpers, e => Assert.Equal(t0, e.BumperTemplateId));
Assert.Equal(0, result.NextBumperIndex); // курсор ротации не двигается
}
[Fact]
public void Bumpers_NoTemplates_NoBumpers()
{
var a = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
var b = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
@@ -338,11 +387,10 @@ public class SchedulePlannerTests
{
Bumpers = new PlannerBumperConfig(
true,
TimeSpan.FromSeconds(8),
OnlyBetweenDifferentShows: true,
MinInterval: TimeSpan.Zero,
Mode: BumperMode.Static,
JinglePool: []
Selection: BumperSelection.Rotation,
Templates: []
),
};
@@ -351,35 +399,6 @@ public class SchedulePlannerTests
Assert.DoesNotContain(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper);
}
[Fact]
public void Bumpers_BothMode_AlternatesDynamicAndStatic()
{
var a = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
var b = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
var j0 = Guid.NewGuid();
var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20), (j0, 1));
// Три перехода: динамика, джингл, динамика.
var input = BaseInput([a, b], durations, Start.AddMinutes(80)) with
{
Bumpers = new PlannerBumperConfig(
true,
TimeSpan.FromSeconds(8),
OnlyBetweenDifferentShows: true,
MinInterval: TimeSpan.Zero,
Mode: BumperMode.Both,
JinglePool: [j0]
),
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));
var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList();
Assert.True(bumpers.Count >= 2);
Assert.Equal(Guid.Empty, bumpers[0].MediaAssetId); // первый — динамический (плейсхолдер)
Assert.Equal(j0, bumpers[1].MediaAssetId); // второй — статичный джингл
}
[Fact]
public void Bumpers_Disabled_ProduceNoBumperEntries()
{
@@ -389,7 +408,7 @@ public class SchedulePlannerTests
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
{
Bumpers = new PlannerBumperConfig(false, TimeSpan.FromSeconds(8), OnlyBetweenDifferentShows: true, MinInterval: TimeSpan.Zero),
Bumpers = Bumper(false, onlyBetweenDifferentShows: true, TimeSpan.Zero),
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));