Enhance channel management: add support for preferred weight multipliers and preferred hours in channel shows, update related data models and API endpoints, and implement validation for new properties. Refactor scheduling logic to utilize preferred hours for weight adjustments during show planning.

This commit is contained in:
Leonid Pershin
2026-07-25 18:44:28 +03:00
parent 358adbff21
commit 8718e8b6bf
36 changed files with 2921 additions and 97 deletions
@@ -415,6 +415,108 @@ public class SchedulePlannerTests
Assert.DoesNotContain(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper);
}
[Fact]
public void Bumpers_ShowChangeChanceZero_SuppressesBumpersOnShowChange()
{
// Два шоу, ротация выбора подблока (без random в пути заставки) → чередование гарантировано,
// каждый переход — смена шоу. ShowChangeChance=0 глушит их, EpisodeChangeChance=1 не при чём.
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));
var input = BaseInput([a, b], durations, Start.AddMinutes(120)) with
{
Bumpers = Bumper(true, BumperTrigger.Both, TimeSpan.Zero) with { ShowChangeChance = 0.0 },
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));
Assert.DoesNotContain(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper);
}
[Fact]
public void Bumpers_EpisodeChangeChanceZero_SuppressesBumpersBetweenEpisodes()
{
// Одно шоу → каждый переход между блоками одного шоу. EpisodeChangeChance=0 глушит все заставки.
var a = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
var durations = Durations((a.EpisodeAssetIds[0], 20));
var input = BaseInput([a], durations, Start.AddMinutes(120)) with
{
Bumpers = Bumper(true, BumperTrigger.Both, TimeSpan.Zero) with { EpisodeChangeChance = 0.0 },
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
Assert.DoesNotContain(result.Entries, e => e.Kind == ScheduleEntryKind.Bumper);
}
[Fact]
public void Bumpers_WeightedRandom_NeverPicksZeroWeightVariant()
{
// Взвешенный выбор: подблок с весом 0 не выбирается никогда, весь трафик уходит на t1 (вес 5).
var a = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
var durations = Durations((a.EpisodeAssetIds[0], 20));
Guid t0 = Guid.NewGuid(),
t1 = Guid.NewGuid();
var input = BaseInput([a], durations, Start.AddMinutes(120)) with
{
Bumpers = Bumper(
true,
BumperTrigger.Both,
TimeSpan.Zero,
BumperSelection.WeightedRandom,
new PlannerBumperVariant(Guid.NewGuid(), t0, TimeSpan.FromSeconds(8), BumperTrigger.Both, 0),
new PlannerBumperVariant(Guid.NewGuid(), t1, TimeSpan.FromSeconds(8), BumperTrigger.Both, 5)
),
};
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1, 2, 3, 4));
var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList();
Assert.NotEmpty(bumpers);
Assert.All(bumpers, e => Assert.Equal(t1, e.BumperTemplateId));
}
[Fact]
public void PreferredHours_BoostsWeight_InsideWindow()
{
// Час старта — 0 (UTC). Окно b покрывает его, множитель 100 → b доминирует, хотя базовые веса равны.
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,
PreferredHours: [new PlannerHourWindow(0, 24)], PreferredWeightMultiplier: 100);
var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20));
var input = BaseInput([a, b], durations, Start.AddMinutes(60));
var result = SchedulePlanner.Plan(input, new FixedRandom(50));
var programs = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Program).ToList();
Assert.NotEmpty(programs);
Assert.All(programs, e => Assert.Equal(b.ShowId, e.ShowId));
}
[Fact]
public void PreferredHours_OutsideWindow_KeepsBaseWeight()
{
// Час старта — 0, окно b — [10,12): не покрывает → буста нет, при FixedRandom(50) берётся первое шоу (a).
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,
PreferredHours: [new PlannerHourWindow(10, 12)], PreferredWeightMultiplier: 100);
var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20));
var input = BaseInput([a, b], durations, Start.AddMinutes(60));
var result = SchedulePlanner.Plan(input, new FixedRandom(50));
var programs = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Program).ToList();
Assert.NotEmpty(programs);
Assert.All(programs, e => Assert.Equal(a.ShowId, e.ShowId));
}
[Fact]
public void NoPlayableShows_ReturnsEmpty()
{