Enhance GridScheduleGenerator to prevent reopening aired slots during rebuilds
Updated the GridScheduleGenerator to include logic that checks if a slot has already aired before planning it again, preventing duplicate scheduling of programs. Introduced a new method, LoadAiredSlotWindowsAsync, to retrieve aired time windows and updated the planning logic accordingly. Added integration tests to verify that slots that have already aired are not reopened during rebuilds, ensuring accurate scheduling behavior. Updated documentation to reflect these changes.
This commit is contained in:
@@ -274,6 +274,53 @@ public sealed class GridScheduleGeneratorIntegrationTests(PostgresFixture fixtur
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пересборка посреди длинного фильма не открывает его слот заново. Кино кончается в 23:14,
|
||||
/// окно слота идёт до 23:15 — и без проверки «выход уже состоялся» планировщик честно ставил
|
||||
/// вторую полнометражку подряд: слот-то по времени ещё не истёк.
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task Generate_Rebuild_DoesNotReopenSlotThatAlreadyAired()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var world = await SeedFilmsAsync(seedDb);
|
||||
|
||||
await using (var first = fixture.CreateContext())
|
||||
await Generator(first).GenerateAsync(world.ChannelId, Now, false, default);
|
||||
|
||||
await using var read = fixture.CreateContext();
|
||||
var film = read
|
||||
.ScheduleEntries.Where(e =>
|
||||
e.ChannelId == world.ChannelId
|
||||
&& e.SlotId == world.SlotId
|
||||
&& e.Kind == ScheduleEntryKind.Program
|
||||
)
|
||||
.OrderBy(e => e.StartsAtUtc)
|
||||
.First();
|
||||
|
||||
var during = film.StartsAtUtc.AddMinutes(30);
|
||||
await using (var rebuild = fixture.CreateContext())
|
||||
await Generator(rebuild).GenerateAsync(world.ChannelId, during, true, default);
|
||||
|
||||
await using var verify = fixture.CreateContext();
|
||||
// Второго кино из того же слота быть не должно; следующее окно этого слота — только завтра.
|
||||
var extra = verify
|
||||
.ScheduleEntries.Where(e =>
|
||||
e.ChannelId == world.ChannelId
|
||||
&& e.SlotId == world.SlotId
|
||||
&& e.Kind == ScheduleEntryKind.Program
|
||||
&& e.StartsAtUtc >= during
|
||||
&& e.StartsAtUtc < film.StartsAtUtc.AddHours(4)
|
||||
)
|
||||
.ToList();
|
||||
|
||||
Assert.Empty(extra);
|
||||
// Само кино при этом не тронуто: прошлое и идущая запись неприкосновенны.
|
||||
Assert.NotNull(verify.ScheduleEntries.FirstOrDefault(e => e.Id == film.Id));
|
||||
}
|
||||
|
||||
// ── Обвязка ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static GridScheduleGenerator Generator(AppDbContext db)
|
||||
@@ -367,6 +414,55 @@ public sealed class GridScheduleGeneratorIntegrationTests(PostgresFixture fixtur
|
||||
return new World(channel.Id, template.Id, slot.Id, show.Id, collection?.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Канал с одним вечерним слотом кино: фильм длиннее слота и доигрывает целиком
|
||||
/// (<see cref="OverflowPolicy.ExtendSlot"/>) — на этом и ловится повторное открытие слота.
|
||||
/// </summary>
|
||||
private static async Task<World> SeedFilmsAsync(AppDbContext db)
|
||||
{
|
||||
var suffix = Guid.NewGuid().ToString("N")[..8];
|
||||
|
||||
var group = Group.Create($"Полнометражки {suffix}");
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var film = Show.Create($"Фильм {i} {suffix}", ShowKind.Single);
|
||||
film.AddEpisode(ReadyAsset(db, $"film{i}-{suffix}.mkv", TimeSpan.FromMinutes(139)).Id);
|
||||
db.Shows.Add(film);
|
||||
group.AddElement(GroupElementKind.Show, film.Id);
|
||||
}
|
||||
db.Groups.Add(group);
|
||||
|
||||
// Филлер подлиннее: паузу до вечера он закрывает по кругу, и минутный дал бы сотни записей.
|
||||
var filler = ReadyAsset(db, $"filler-{suffix}.mkv", TimeSpan.FromMinutes(30));
|
||||
|
||||
var channel = Channel.Create($"Канал {suffix}", $"ch-{suffix}", Now.AddDays(-7));
|
||||
channel.UpdateSettings(channel.Name, isEnabled: true, filler.Id);
|
||||
|
||||
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
|
||||
var layer = template.AddLayer("Базовый", 10);
|
||||
var slot = layer.AddSlot("Кино в 21:00", new TimeOnly(21, 0), 135);
|
||||
slot.UpdateContent(
|
||||
new SlotContent(
|
||||
slot.Title,
|
||||
SlotKind.Content,
|
||||
group.Id,
|
||||
new SlotStrategy(SlotStrategyType.Sequential).ToJson(),
|
||||
null,
|
||||
SlotBlockMode.Count,
|
||||
1,
|
||||
OverflowPolicy.ExtendSlot
|
||||
)
|
||||
);
|
||||
channel.SetTemplate(template.Id);
|
||||
template.MarkChanged();
|
||||
|
||||
db.Channels.Add(channel);
|
||||
db.ScheduleTemplates.Add(template);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return new World(channel.Id, template.Id, slot.Id, Guid.Empty, null);
|
||||
}
|
||||
|
||||
/// <summary>Ассет, доведённый до Ready: в эфир попадают только такие.</summary>
|
||||
private static MediaAsset ReadyAsset(AppDbContext db, string fileName, TimeSpan duration)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user