From b8d6b6aad24e494042a061530e4d412892d00d43 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Mon, 27 Jul 2026 09:08:20 +0300 Subject: [PATCH] Refactor GenerateGridCommandHandler and GridPlanner for improved slot management and readability Updated the GenerateGridCommandHandler to streamline slot removal during grid generation by introducing a new RemoveAllSlots method. Enhanced the GridPlanner class by refactoring the FillDay method to utilize a SlotTarget struct for better clarity and organization. These changes improve code maintainability and readability while ensuring efficient slot handling across grid generation processes. --- .../Generate/GenerateGridCommandHandler.cs | 24 ++- .../Templates/Generate/GridPlanner.cs | 138 +++++++++--------- .../Templates/Generate/GridProfiles.cs | 74 ++++++---- .../Restore/RestoreTemplateCommandHandler.cs | 20 ++- .../Validate/ValidateTemplateQueryHandler.cs | 6 +- .../Media/FileSystemMediaStorage.cs | 9 +- 6 files changed, 155 insertions(+), 116 deletions(-) diff --git a/backend/src/TeleWave.Application/Programming/Templates/Generate/GenerateGridCommandHandler.cs b/backend/src/TeleWave.Application/Programming/Templates/Generate/GenerateGridCommandHandler.cs index 8d09d77..0a5f1dd 100644 --- a/backend/src/TeleWave.Application/Programming/Templates/Generate/GenerateGridCommandHandler.cs +++ b/backend/src/TeleWave.Application/Programming/Templates/Generate/GenerateGridCommandHandler.cs @@ -48,12 +48,7 @@ public sealed class GenerateGridCommandHandler( if (template is null) return Result.Failure(ChannelErrors.TemplateNotFound); - var removed = 0; - if (command.Mode == GridGenerationMode.Rebuild) - foreach (var layer in template.Layers) - foreach (var slot in layer.Slots.ToList()) - if (layer.RemoveSlot(slot.Id)) - removed++; + var removed = command.Mode == GridGenerationMode.Rebuild ? RemoveAllSlots(template) : 0; var created = 0; foreach (var slot in plan.Slots) @@ -74,6 +69,23 @@ public sealed class GenerateGridCommandHandler( return Result.Success(new GenerateGridResultDto(created, removed)); } + /// Снимает все слоты шаблона и возвращает их число — режим «собрать с нуля». + private static int RemoveAllSlots(ScheduleTemplate template) + { + var removed = 0; + + foreach (var layer in template.Layers) + { + // Идентификаторы снимаем заранее: удаление правит ту самую коллекцию, по которой идём. + var slotIds = layer.Slots.Select(s => s.Id).ToList(); + foreach (var slotId in slotIds) + layer.RemoveSlot(slotId); + removed += slotIds.Count; + } + + return removed; + } + private static GridLayer EnsureLayer(ScheduleTemplate template, GridPlanLayer kind) { var (name, priority) = diff --git a/backend/src/TeleWave.Application/Programming/Templates/Generate/GridPlanner.cs b/backend/src/TeleWave.Application/Programming/Templates/Generate/GridPlanner.cs index 8c3edba..c9f5b7c 100644 --- a/backend/src/TeleWave.Application/Programming/Templates/Generate/GridPlanner.cs +++ b/backend/src/TeleWave.Application/Programming/Templates/Generate/GridPlanner.cs @@ -110,15 +110,21 @@ public sealed class GridPlanner( private static List BuildWeek(PlanRun run) { var wholeDay = new List<(int From, int To)> { (0, GridCoverage.MinutesInDay) }; - var slots = FillDay(run.Profile.Weekdays, wholeDay, null, GridPlanLayer.Main, run); + var slots = FillDay( + run.Profile.Weekdays, + wholeDay, + new SlotTarget(null, GridPlanLayer.Main), + run + ); if (run.Profile.Weekend.Count == 0) return slots; foreach (var weekday in new[] { 6, 0 }) - slots.AddRange( - FillDay(run.Profile.Weekend, wholeDay, weekday, GridPlanLayer.Weekend, run) - ); + { + var target = new SlotTarget(weekday, GridPlanLayer.Weekend); + slots.AddRange(FillDay(run.Profile.Weekend, wholeDay, target, run)); + } return slots; } @@ -139,9 +145,8 @@ public sealed class GridPlanner( foreach (var weekday in GridCoverage.Week) { var gaps = GridCoverage.Gaps(existing, weekday, run.DayStart); - foreach ( - var slot in FillDay(run.Profile.Weekdays, gaps, weekday, GridPlanLayer.Main, run) - ) + var target = new SlotTarget(weekday, GridPlanLayer.Main); + foreach (var slot in FillDay(run.Profile.Weekdays, gaps, target, run)) entries.Add((weekday, slot)); } @@ -168,8 +173,7 @@ public sealed class GridPlanner( private static List FillDay( IReadOnlyList bands, IReadOnlyList<(int From, int To)> free, - int? weekday, - GridPlanLayer layer, + SlotTarget target, PlanRun run ) { @@ -178,21 +182,25 @@ public sealed class GridPlanner( var used = new Dictionary(); var slots = new List(); - foreach (var band in bands) - foreach (var (bandFrom, bandTo) in BandPieces(band, run.DayStart)) - foreach (var (freeFrom, freeTo) in free) - { - var from = Math.Max(bandFrom, freeFrom); - var to = Math.Min(bandTo, freeTo); - if (to - from < MinSlotMinutes) - continue; + // Что реально предстоит застроить: полосы профиля, пересечённые со свободным временем. + var pieces = + from band in bands + from piece in BandPieces(band, run.DayStart) + from gap in free + let start = Math.Max(piece.From, gap.From) + let end = Math.Min(piece.To, gap.To) + where end - start >= MinSlotMinutes + select (Band: band, From: start, To: end); - slots.AddRange(FillPiece(band, from, to, weekday, layer, run, used)); - } + foreach (var piece in pieces) + slots.AddRange(FillPiece(piece.Band, piece.From, piece.To, target, run, used)); return [.. slots.OrderBy(s => GridCoverage.OffsetInDay(s.Start, run.DayStart))]; } + /// Куда ложится слот: день недели и слой. Пара ходит вместе через всю раскладку. + private readonly record struct SlotTarget(int? Weekday, GridPlanLayer Layer); + /// /// Полоса в смещениях от начала вещательных суток. Начало суток канала произвольно, поэтому /// полоса может перевалить через их конец — тогда она разрезается на два куска. @@ -217,8 +225,7 @@ public sealed class GridPlanner( GridBand band, int from, int to, - int? weekday, - GridPlanLayer layer, + SlotTarget target, PlanRun run, Dictionary used ) @@ -250,7 +257,7 @@ public sealed class GridPlanner( // Якорь — только на первом слоте прайма: жёсткий старт у каждого слота подряд заставил // бы генератор крутить фон перед каждым из них. var anchor = cursor == from && band.Daypart == Daypart.Prime; - var slot = Compose(band, weekday, layer, cursor, duration, anchor, run, used); + var slot = Compose(band, target, cursor, duration, anchor, run, used); if (slot is not null) yield return slot; @@ -260,8 +267,7 @@ public sealed class GridPlanner( private static PlannedSlot? Compose( GridBand band, - int? weekday, - GridPlanLayer layer, + SlotTarget target, int offset, int duration, bool anchor, @@ -295,7 +301,7 @@ public sealed class GridPlanner( } used[group.Id] = used.GetValueOrDefault(group.Id) + 1; - run.WarnIfThin(band, group, weekday); + run.WarnIfThin(band, group, target.Weekday); var blockMode = band.UnitsPerBlock > 0 ? SlotBlockMode.Count : SlotBlockMode.FillSlot; return New( @@ -316,8 +322,8 @@ public sealed class GridPlanner( RepeatSource? repeat ) => new( - layer, - weekday, + target.Layer, + target.Weekday, start, duration, title, @@ -393,52 +399,48 @@ public sealed class GridPlanner( cancellationToken ); - var units = 0; - var duration = TimeSpan.Zero; - var seriesUnits = 0; - var singleUnits = 0; - ShowAudience? strictest = null; - - foreach (var element in composition) - { - if (!info.TryGetValue((element.Kind, element.Id), out var resolved)) - continue; - - units += resolved.UnitCount; - duration += resolved.TotalDuration; - - // У коллекции своего типа нет: франшиза — это почти всегда полнометражки. - if (resolved.ShowKind == ShowKind.Series) - seriesUnits += resolved.UnitCount; - else - singleUnits += resolved.UnitCount; - - if ( - resolved.Audience is { } audience - && (strictest is null || audience > strictest) - ) - strictest = audience; - } - - // Группа без готовых единиц в эфир не годится: слот встал бы, а место закрыл фон. - if (units == 0) - continue; - - candidates.Add( - new GroupCandidate( - group.Id, - group.Name, - units, - strictest, - seriesUnits >= singleUnits ? ShowKind.Series : ShowKind.Single, - duration.TotalMinutes / units - ) - ); + if (Describe(group, composition, info) is { } candidate) + candidates.Add(candidate); } return candidates; } + /// + /// Сводит вычисленный состав к тому, что нужно раскладке: сколько эфира группа даёт, какой она + /// в основном природы и насколько строгий в ней контент. Группа без готовых единиц не годится + /// вовсе — слот по ней встал бы, а место закрыл фон. + /// + private static GroupCandidate? Describe( + Group group, + IReadOnlyList composition, + IReadOnlyDictionary<(GroupElementKind Kind, Guid Id), GroupElementInfo> info + ) + { + var resolved = composition + .Select(e => info.GetValueOrDefault((e.Kind, e.Id))) + .OfType() + .ToList(); + + var units = resolved.Sum(r => r.UnitCount); + if (units == 0) + return null; + + // У коллекции своего типа нет: франшиза — это почти всегда полнометражки. + var seriesUnits = resolved.Where(r => r.ShowKind == ShowKind.Series).Sum(r => r.UnitCount); + var duration = resolved.Aggregate(TimeSpan.Zero, (sum, r) => sum + r.TotalDuration); + var strictest = resolved.Max(r => r.Audience); + + return new GroupCandidate( + group.Id, + group.Name, + units, + strictest, + seriesUnits * 2 >= units ? ShowKind.Series : ShowKind.Single, + duration.TotalMinutes / units + ); + } + /// Группа глазами раскладки: чем её можно закрыть и насколько её хватит. private sealed record GroupCandidate( Guid Id, diff --git a/backend/src/TeleWave.Application/Programming/Templates/Generate/GridProfiles.cs b/backend/src/TeleWave.Application/Programming/Templates/Generate/GridProfiles.cs index 892804e..7bea3e6 100644 --- a/backend/src/TeleWave.Application/Programming/Templates/Generate/GridProfiles.cs +++ b/backend/src/TeleWave.Application/Programming/Templates/Generate/GridProfiles.cs @@ -19,6 +19,20 @@ public static class GridProfiles public static GridProfile Get(GridProfileKind kind) => All.FirstOrDefault(p => p.Kind == kind) ?? All[0]; + // Названия полос, встречающиеся у нескольких профилей. Полосы — данные, и повторяющийся + // заголовок должен быть один: он попадает в название слота и виден админу в сетке. + private const string Morning = "Утро"; + private const string Day = "День"; + private const string DayBlock = "Дневной блок"; + private const string DayMarathon = "Дневной марафон"; + private const string Prime = "Прайм"; + private const string Night = "Ночь"; + private const string NightAir = "Ночной эфир"; + private const string MorningMarathon = "Утренний марафон"; + private const string GoodNight = "Спокойной ночи"; + private const string SignOffTitle = "Конец вещания"; + private const string PrimeMovie = "Кино в прайм"; + private static TimeOnly At(int hour, int minute = 0) => new(hour, minute); /// Ночной взрослый блок — общая концовка суток почти у всех профилей. @@ -46,7 +60,7 @@ public static class GridProfiles At(6), At(10), Daypart.Morning, - "Утро", + Morning, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 120, @@ -56,7 +70,7 @@ public static class GridProfiles At(10), At(14), Daypart.Day, - "День", + Day, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 120, @@ -66,7 +80,7 @@ public static class GridProfiles At(14), At(18), Daypart.Day, - "Дневной блок", + DayBlock, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 120, @@ -86,7 +100,7 @@ public static class GridProfiles At(20), At(23), Daypart.Prime, - "Прайм", + Prime, ShowKind.Single, BlockMinutes: 150, UnitsPerBlock: 1, @@ -94,7 +108,7 @@ public static class GridProfiles CooldownDays: 14 ), AdultNight(At(23), At(2)), - new(At(2), At(6), Daypart.Night, "Ночной эфир", BlockMinutes: 240), + new(At(2), At(6), Daypart.Night, NightAir, BlockMinutes: 240), ], [ new( @@ -111,7 +125,7 @@ public static class GridProfiles At(10), At(14), Daypart.Day, - "Дневной марафон", + DayMarathon, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 240 @@ -132,7 +146,7 @@ public static class GridProfiles At(18), At(23), Daypart.Prime, - "Кино в прайм", + PrimeMovie, ShowKind.Single, BlockMinutes: 150, UnitsPerBlock: 1, @@ -140,7 +154,7 @@ public static class GridProfiles CooldownDays: 21 ), AdultNight(At(23), At(2)), - new(At(2), At(6), Daypart.Night, "Ночной эфир", BlockMinutes: 240), + new(At(2), At(6), Daypart.Night, NightAir, BlockMinutes: 240), ] ); @@ -166,7 +180,7 @@ public static class GridProfiles At(10), At(14), Daypart.Day, - "Дневной блок", + DayBlock, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 120, @@ -176,7 +190,7 @@ public static class GridProfiles At(14), At(19), Daypart.Day, - "Дневной марафон", + DayMarathon, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 150, @@ -186,21 +200,21 @@ public static class GridProfiles At(19), At(23), Daypart.Prime, - "Прайм", + Prime, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 120, UnitsPerBlock: 4 ), AdultNight(At(23), At(2)), - new(At(2), At(6), Daypart.Night, "Ночь", ShowKind.Series, BlockMinutes: 240), + new(At(2), At(6), Daypart.Night, Night, ShowKind.Series, BlockMinutes: 240), ], [ new( At(6), At(12), Daypart.Morning, - "Утренний марафон", + MorningMarathon, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 180, @@ -210,7 +224,7 @@ public static class GridProfiles At(12), At(19), Daypart.Day, - "Дневной марафон", + DayMarathon, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 210 @@ -219,14 +233,14 @@ public static class GridProfiles At(19), At(23), Daypart.Prime, - "Прайм", + Prime, ShowKind.Series, ShowAudience.Pg13, BlockMinutes: 120, UnitsPerBlock: 4 ), AdultNight(At(23), At(3)), - new(At(3), At(6), Daypart.Night, "Ночь", ShowKind.Series, BlockMinutes: 180), + new(At(3), At(6), Daypart.Night, Night, ShowKind.Series, BlockMinutes: 180), ] ); @@ -346,7 +360,7 @@ public static class GridProfiles At(14), At(18), Daypart.Day, - "Дневной блок", + DayBlock, BlockMinutes: 240, Strategy: SlotStrategyType.RandomWithCooldown, CooldownDays: 2 @@ -373,7 +387,7 @@ public static class GridProfiles At(2), At(6), Daypart.Night, - "Ночь", + Night, BlockMinutes: 240, Strategy: SlotStrategyType.RandomWithCooldown, CooldownDays: 1 @@ -418,7 +432,7 @@ public static class GridProfiles At(18), At(23), Daypart.Prime, - "Кино в прайм", + PrimeMovie, ShowKind.Single, BlockMinutes: 150, UnitsPerBlock: 1, @@ -441,7 +455,7 @@ public static class GridProfiles At(2), At(6), Daypart.Night, - "Ночной эфир", + NightAir, ShowKind.Single, BlockMinutes: 120, UnitsPerBlock: 1, @@ -464,7 +478,7 @@ public static class GridProfiles At(6), At(10), Daypart.Morning, - "Утро", + Morning, ShowKind.Series, ShowAudience.Pg, BlockMinutes: 90, @@ -474,7 +488,7 @@ public static class GridProfiles At(10), At(14), Daypart.Day, - "День", + Day, ShowKind.Series, ShowAudience.Pg, BlockMinutes: 90, @@ -484,7 +498,7 @@ public static class GridProfiles At(14), At(18), Daypart.Day, - "Дневной блок", + DayBlock, ShowKind.Series, ShowAudience.Pg, BlockMinutes: 90, @@ -504,7 +518,7 @@ public static class GridProfiles At(21), At(22), Daypart.Prime, - "Спокойной ночи", + GoodNight, ShowKind.Series, ShowAudience.Pg, BlockMinutes: 60, @@ -514,7 +528,7 @@ public static class GridProfiles At(22), At(6), Daypart.Night, - "Конец вещания", + SignOffTitle, SlotKind: SlotKind.SignOff, BlockMinutes: 0 ), @@ -524,7 +538,7 @@ public static class GridProfiles At(6), At(10), Daypart.Morning, - "Утренний марафон", + MorningMarathon, ShowKind.Series, ShowAudience.Pg, BlockMinutes: 240 @@ -533,7 +547,7 @@ public static class GridProfiles At(10), At(14), Daypart.Day, - "День", + Day, ShowKind.Series, ShowAudience.Pg, BlockMinutes: 120, @@ -543,7 +557,7 @@ public static class GridProfiles At(14), At(18), Daypart.Day, - "Дневной блок", + DayBlock, ShowKind.Series, ShowAudience.Pg, BlockMinutes: 120, @@ -565,7 +579,7 @@ public static class GridProfiles At(21), At(22), Daypart.Prime, - "Спокойной ночи", + GoodNight, ShowKind.Series, ShowAudience.Pg, BlockMinutes: 60, @@ -575,7 +589,7 @@ public static class GridProfiles At(22), At(6), Daypart.Night, - "Конец вещания", + SignOffTitle, SlotKind: SlotKind.SignOff, BlockMinutes: 0 ), diff --git a/backend/src/TeleWave.Application/Programming/Templates/Restore/RestoreTemplateCommandHandler.cs b/backend/src/TeleWave.Application/Programming/Templates/Restore/RestoreTemplateCommandHandler.cs index 599fa8f..4585e33 100644 --- a/backend/src/TeleWave.Application/Programming/Templates/Restore/RestoreTemplateCommandHandler.cs +++ b/backend/src/TeleWave.Application/Programming/Templates/Restore/RestoreTemplateCommandHandler.cs @@ -38,9 +38,14 @@ public sealed class RestoreTemplateCommandHandler(IAppDbContext dbContext) // Слои, которых в снимке нет, — заведены после применения. Фоновый не трогаем: он есть // в любом шаблоне и удалению не подлежит. - foreach (var layer in template.Layers.ToList()) - if (!layer.IsBackground && snapshot.Layers.All(l => l.Id != layer.Id)) - template.RemoveLayer(layer.Id); + var staleLayers = template + .Layers.Where(layer => + !layer.IsBackground && snapshot.Layers.All(l => l.Id != layer.Id) + ) + .Select(layer => layer.Id) + .ToList(); + foreach (var layerId in staleLayers) + template.RemoveLayer(layerId); foreach (var layerSnapshot in snapshot.Layers) RestoreLayer(template, layerSnapshot, refs); @@ -76,9 +81,12 @@ public sealed class RestoreTemplateCommandHandler(IAppDbContext dbContext) snapshot.IsEnabled ); - foreach (var slot in layer.Slots.ToList()) - if (snapshot.Slots.All(s => s.Id != slot.Id)) - layer.RemoveSlot(slot.Id); + var staleSlots = layer + .Slots.Where(slot => snapshot.Slots.All(s => s.Id != slot.Id)) + .Select(slot => slot.Id) + .ToList(); + foreach (var slotId in staleSlots) + layer.RemoveSlot(slotId); foreach (var slotSnapshot in snapshot.Slots) RestoreSlot(layer, slotSnapshot, refs); diff --git a/backend/src/TeleWave.Application/Programming/Templates/Validate/ValidateTemplateQueryHandler.cs b/backend/src/TeleWave.Application/Programming/Templates/Validate/ValidateTemplateQueryHandler.cs index c1d0f58..df081ff 100644 --- a/backend/src/TeleWave.Application/Programming/Templates/Validate/ValidateTemplateQueryHandler.cs +++ b/backend/src/TeleWave.Application/Programming/Templates/Validate/ValidateTemplateQueryHandler.cs @@ -212,9 +212,9 @@ public sealed class ValidateTemplateQueryHandler( { var slots = layers.SelectMany(l => l.Slots).ToList(); - foreach (var weekday in GridCoverage.Week) - foreach (var (from, to) in GridCoverage.Gaps(slots, weekday, dayStart)) - yield return Gap(weekday, from, to, dayStart); + return from weekday in GridCoverage.Week + from gap in GridCoverage.Gaps(slots, weekday, dayStart) + select Gap(weekday, gap.From, gap.To, dayStart); } private static TemplateIssueDto Gap(int weekday, int from, int to, TimeOnly dayStart) diff --git a/backend/src/TeleWave.Infrastructure/Media/FileSystemMediaStorage.cs b/backend/src/TeleWave.Infrastructure/Media/FileSystemMediaStorage.cs index 743c33f..908acd4 100644 --- a/backend/src/TeleWave.Infrastructure/Media/FileSystemMediaStorage.cs +++ b/backend/src/TeleWave.Infrastructure/Media/FileSystemMediaStorage.cs @@ -152,10 +152,13 @@ public sealed class FileSystemMediaStorage(MediaPathResolver paths) : IMediaStor private static void DeleteCompanions(string directory, CompanionMatcher companion) { - foreach (var file in Directory.EnumerateFiles(directory)) + var companions = Directory + .EnumerateFiles(directory) + .Where(file => companion.Matches(Path.GetFileName(file))) + .ToList(); + + foreach (var file in companions) { - if (!companion.Matches(Path.GetFileName(file))) - continue; try { File.Delete(file);