Refactor GenerateGridCommandHandler and GridPlanner for improved slot management and readability
ci / build-backend (push) Successful in 2m45s
ci / build-frontend (push) Successful in 1m11s
ci / tests (push) Successful in 2m45s
ci / sonar (push) Successful in 6m33s

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.
This commit is contained in:
Leonid Pershin
2026-07-27 09:08:20 +03:00
parent f22aee4685
commit b8d6b6aad2
6 changed files with 155 additions and 116 deletions
@@ -48,12 +48,7 @@ public sealed class GenerateGridCommandHandler(
if (template is null) if (template is null)
return Result.Failure<GenerateGridResultDto>(ChannelErrors.TemplateNotFound); return Result.Failure<GenerateGridResultDto>(ChannelErrors.TemplateNotFound);
var removed = 0; var removed = command.Mode == GridGenerationMode.Rebuild ? RemoveAllSlots(template) : 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 created = 0; var created = 0;
foreach (var slot in plan.Slots) foreach (var slot in plan.Slots)
@@ -74,6 +69,23 @@ public sealed class GenerateGridCommandHandler(
return Result.Success(new GenerateGridResultDto(created, removed)); return Result.Success(new GenerateGridResultDto(created, removed));
} }
/// <summary>Снимает все слоты шаблона и возвращает их число — режим «собрать с нуля».</summary>
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) private static GridLayer EnsureLayer(ScheduleTemplate template, GridPlanLayer kind)
{ {
var (name, priority) = var (name, priority) =
@@ -110,15 +110,21 @@ public sealed class GridPlanner(
private static List<PlannedSlot> BuildWeek(PlanRun run) private static List<PlannedSlot> BuildWeek(PlanRun run)
{ {
var wholeDay = new List<(int From, int To)> { (0, GridCoverage.MinutesInDay) }; 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) if (run.Profile.Weekend.Count == 0)
return slots; return slots;
foreach (var weekday in new[] { 6, 0 }) 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; return slots;
} }
@@ -139,9 +145,8 @@ public sealed class GridPlanner(
foreach (var weekday in GridCoverage.Week) foreach (var weekday in GridCoverage.Week)
{ {
var gaps = GridCoverage.Gaps(existing, weekday, run.DayStart); var gaps = GridCoverage.Gaps(existing, weekday, run.DayStart);
foreach ( var target = new SlotTarget(weekday, GridPlanLayer.Main);
var slot in FillDay(run.Profile.Weekdays, gaps, weekday, GridPlanLayer.Main, run) foreach (var slot in FillDay(run.Profile.Weekdays, gaps, target, run))
)
entries.Add((weekday, slot)); entries.Add((weekday, slot));
} }
@@ -168,8 +173,7 @@ public sealed class GridPlanner(
private static List<PlannedSlot> FillDay( private static List<PlannedSlot> FillDay(
IReadOnlyList<GridBand> bands, IReadOnlyList<GridBand> bands,
IReadOnlyList<(int From, int To)> free, IReadOnlyList<(int From, int To)> free,
int? weekday, SlotTarget target,
GridPlanLayer layer,
PlanRun run PlanRun run
) )
{ {
@@ -178,21 +182,25 @@ public sealed class GridPlanner(
var used = new Dictionary<Guid, int>(); var used = new Dictionary<Guid, int>();
var slots = new List<PlannedSlot>(); var slots = new List<PlannedSlot>();
foreach (var band in bands) // Что реально предстоит застроить: полосы профиля, пересечённые со свободным временем.
foreach (var (bandFrom, bandTo) in BandPieces(band, run.DayStart)) var pieces =
foreach (var (freeFrom, freeTo) in free) from band in bands
{ from piece in BandPieces(band, run.DayStart)
var from = Math.Max(bandFrom, freeFrom); from gap in free
var to = Math.Min(bandTo, freeTo); let start = Math.Max(piece.From, gap.From)
if (to - from < MinSlotMinutes) let end = Math.Min(piece.To, gap.To)
continue; 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))]; return [.. slots.OrderBy(s => GridCoverage.OffsetInDay(s.Start, run.DayStart))];
} }
/// <summary>Куда ложится слот: день недели и слой. Пара ходит вместе через всю раскладку.</summary>
private readonly record struct SlotTarget(int? Weekday, GridPlanLayer Layer);
/// <summary> /// <summary>
/// Полоса в смещениях от начала вещательных суток. Начало суток канала произвольно, поэтому /// Полоса в смещениях от начала вещательных суток. Начало суток канала произвольно, поэтому
/// полоса может перевалить через их конец — тогда она разрезается на два куска. /// полоса может перевалить через их конец — тогда она разрезается на два куска.
@@ -217,8 +225,7 @@ public sealed class GridPlanner(
GridBand band, GridBand band,
int from, int from,
int to, int to,
int? weekday, SlotTarget target,
GridPlanLayer layer,
PlanRun run, PlanRun run,
Dictionary<Guid, int> used Dictionary<Guid, int> used
) )
@@ -250,7 +257,7 @@ public sealed class GridPlanner(
// Якорь — только на первом слоте прайма: жёсткий старт у каждого слота подряд заставил // Якорь — только на первом слоте прайма: жёсткий старт у каждого слота подряд заставил
// бы генератор крутить фон перед каждым из них. // бы генератор крутить фон перед каждым из них.
var anchor = cursor == from && band.Daypart == Daypart.Prime; 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) if (slot is not null)
yield return slot; yield return slot;
@@ -260,8 +267,7 @@ public sealed class GridPlanner(
private static PlannedSlot? Compose( private static PlannedSlot? Compose(
GridBand band, GridBand band,
int? weekday, SlotTarget target,
GridPlanLayer layer,
int offset, int offset,
int duration, int duration,
bool anchor, bool anchor,
@@ -295,7 +301,7 @@ public sealed class GridPlanner(
} }
used[group.Id] = used.GetValueOrDefault(group.Id) + 1; 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; var blockMode = band.UnitsPerBlock > 0 ? SlotBlockMode.Count : SlotBlockMode.FillSlot;
return New( return New(
@@ -316,8 +322,8 @@ public sealed class GridPlanner(
RepeatSource? repeat RepeatSource? repeat
) => ) =>
new( new(
layer, target.Layer,
weekday, target.Weekday,
start, start,
duration, duration,
title, title,
@@ -393,52 +399,48 @@ public sealed class GridPlanner(
cancellationToken cancellationToken
); );
var units = 0; if (Describe(group, composition, info) is { } candidate)
var duration = TimeSpan.Zero; candidates.Add(candidate);
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
)
);
} }
return candidates; return candidates;
} }
/// <summary>
/// Сводит вычисленный состав к тому, что нужно раскладке: сколько эфира группа даёт, какой она
/// в основном природы и насколько строгий в ней контент. Группа без готовых единиц не годится
/// вовсе — слот по ней встал бы, а место закрыл фон.
/// </summary>
private static GroupCandidate? Describe(
Group group,
IReadOnlyList<ResolvedElement> composition,
IReadOnlyDictionary<(GroupElementKind Kind, Guid Id), GroupElementInfo> info
)
{
var resolved = composition
.Select(e => info.GetValueOrDefault((e.Kind, e.Id)))
.OfType<GroupElementInfo>()
.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
);
}
/// <summary>Группа глазами раскладки: чем её можно закрыть и насколько её хватит.</summary> /// <summary>Группа глазами раскладки: чем её можно закрыть и насколько её хватит.</summary>
private sealed record GroupCandidate( private sealed record GroupCandidate(
Guid Id, Guid Id,
@@ -19,6 +19,20 @@ public static class GridProfiles
public static GridProfile Get(GridProfileKind kind) => public static GridProfile Get(GridProfileKind kind) =>
All.FirstOrDefault(p => p.Kind == kind) ?? All[0]; 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); private static TimeOnly At(int hour, int minute = 0) => new(hour, minute);
/// <summary>Ночной взрослый блок — общая концовка суток почти у всех профилей.</summary> /// <summary>Ночной взрослый блок — общая концовка суток почти у всех профилей.</summary>
@@ -46,7 +60,7 @@ public static class GridProfiles
At(6), At(6),
At(10), At(10),
Daypart.Morning, Daypart.Morning,
"Утро", Morning,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 120, BlockMinutes: 120,
@@ -56,7 +70,7 @@ public static class GridProfiles
At(10), At(10),
At(14), At(14),
Daypart.Day, Daypart.Day,
"День", Day,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 120, BlockMinutes: 120,
@@ -66,7 +80,7 @@ public static class GridProfiles
At(14), At(14),
At(18), At(18),
Daypart.Day, Daypart.Day,
"Дневной блок", DayBlock,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 120, BlockMinutes: 120,
@@ -86,7 +100,7 @@ public static class GridProfiles
At(20), At(20),
At(23), At(23),
Daypart.Prime, Daypart.Prime,
"Прайм", Prime,
ShowKind.Single, ShowKind.Single,
BlockMinutes: 150, BlockMinutes: 150,
UnitsPerBlock: 1, UnitsPerBlock: 1,
@@ -94,7 +108,7 @@ public static class GridProfiles
CooldownDays: 14 CooldownDays: 14
), ),
AdultNight(At(23), At(2)), 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( new(
@@ -111,7 +125,7 @@ public static class GridProfiles
At(10), At(10),
At(14), At(14),
Daypart.Day, Daypart.Day,
"Дневной марафон", DayMarathon,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 240 BlockMinutes: 240
@@ -132,7 +146,7 @@ public static class GridProfiles
At(18), At(18),
At(23), At(23),
Daypart.Prime, Daypart.Prime,
"Кино в прайм", PrimeMovie,
ShowKind.Single, ShowKind.Single,
BlockMinutes: 150, BlockMinutes: 150,
UnitsPerBlock: 1, UnitsPerBlock: 1,
@@ -140,7 +154,7 @@ public static class GridProfiles
CooldownDays: 21 CooldownDays: 21
), ),
AdultNight(At(23), At(2)), 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(10),
At(14), At(14),
Daypart.Day, Daypart.Day,
"Дневной блок", DayBlock,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 120, BlockMinutes: 120,
@@ -176,7 +190,7 @@ public static class GridProfiles
At(14), At(14),
At(19), At(19),
Daypart.Day, Daypart.Day,
"Дневной марафон", DayMarathon,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 150, BlockMinutes: 150,
@@ -186,21 +200,21 @@ public static class GridProfiles
At(19), At(19),
At(23), At(23),
Daypart.Prime, Daypart.Prime,
"Прайм", Prime,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 120, BlockMinutes: 120,
UnitsPerBlock: 4 UnitsPerBlock: 4
), ),
AdultNight(At(23), At(2)), 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( new(
At(6), At(6),
At(12), At(12),
Daypart.Morning, Daypart.Morning,
"Утренний марафон", MorningMarathon,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 180, BlockMinutes: 180,
@@ -210,7 +224,7 @@ public static class GridProfiles
At(12), At(12),
At(19), At(19),
Daypart.Day, Daypart.Day,
"Дневной марафон", DayMarathon,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 210 BlockMinutes: 210
@@ -219,14 +233,14 @@ public static class GridProfiles
At(19), At(19),
At(23), At(23),
Daypart.Prime, Daypart.Prime,
"Прайм", Prime,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg13, ShowAudience.Pg13,
BlockMinutes: 120, BlockMinutes: 120,
UnitsPerBlock: 4 UnitsPerBlock: 4
), ),
AdultNight(At(23), At(3)), 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(14),
At(18), At(18),
Daypart.Day, Daypart.Day,
"Дневной блок", DayBlock,
BlockMinutes: 240, BlockMinutes: 240,
Strategy: SlotStrategyType.RandomWithCooldown, Strategy: SlotStrategyType.RandomWithCooldown,
CooldownDays: 2 CooldownDays: 2
@@ -373,7 +387,7 @@ public static class GridProfiles
At(2), At(2),
At(6), At(6),
Daypart.Night, Daypart.Night,
"Ночь", Night,
BlockMinutes: 240, BlockMinutes: 240,
Strategy: SlotStrategyType.RandomWithCooldown, Strategy: SlotStrategyType.RandomWithCooldown,
CooldownDays: 1 CooldownDays: 1
@@ -418,7 +432,7 @@ public static class GridProfiles
At(18), At(18),
At(23), At(23),
Daypart.Prime, Daypart.Prime,
"Кино в прайм", PrimeMovie,
ShowKind.Single, ShowKind.Single,
BlockMinutes: 150, BlockMinutes: 150,
UnitsPerBlock: 1, UnitsPerBlock: 1,
@@ -441,7 +455,7 @@ public static class GridProfiles
At(2), At(2),
At(6), At(6),
Daypart.Night, Daypart.Night,
"Ночной эфир", NightAir,
ShowKind.Single, ShowKind.Single,
BlockMinutes: 120, BlockMinutes: 120,
UnitsPerBlock: 1, UnitsPerBlock: 1,
@@ -464,7 +478,7 @@ public static class GridProfiles
At(6), At(6),
At(10), At(10),
Daypart.Morning, Daypart.Morning,
"Утро", Morning,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg, ShowAudience.Pg,
BlockMinutes: 90, BlockMinutes: 90,
@@ -474,7 +488,7 @@ public static class GridProfiles
At(10), At(10),
At(14), At(14),
Daypart.Day, Daypart.Day,
"День", Day,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg, ShowAudience.Pg,
BlockMinutes: 90, BlockMinutes: 90,
@@ -484,7 +498,7 @@ public static class GridProfiles
At(14), At(14),
At(18), At(18),
Daypart.Day, Daypart.Day,
"Дневной блок", DayBlock,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg, ShowAudience.Pg,
BlockMinutes: 90, BlockMinutes: 90,
@@ -504,7 +518,7 @@ public static class GridProfiles
At(21), At(21),
At(22), At(22),
Daypart.Prime, Daypart.Prime,
"Спокойной ночи", GoodNight,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg, ShowAudience.Pg,
BlockMinutes: 60, BlockMinutes: 60,
@@ -514,7 +528,7 @@ public static class GridProfiles
At(22), At(22),
At(6), At(6),
Daypart.Night, Daypart.Night,
"Конец вещания", SignOffTitle,
SlotKind: SlotKind.SignOff, SlotKind: SlotKind.SignOff,
BlockMinutes: 0 BlockMinutes: 0
), ),
@@ -524,7 +538,7 @@ public static class GridProfiles
At(6), At(6),
At(10), At(10),
Daypart.Morning, Daypart.Morning,
"Утренний марафон", MorningMarathon,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg, ShowAudience.Pg,
BlockMinutes: 240 BlockMinutes: 240
@@ -533,7 +547,7 @@ public static class GridProfiles
At(10), At(10),
At(14), At(14),
Daypart.Day, Daypart.Day,
"День", Day,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg, ShowAudience.Pg,
BlockMinutes: 120, BlockMinutes: 120,
@@ -543,7 +557,7 @@ public static class GridProfiles
At(14), At(14),
At(18), At(18),
Daypart.Day, Daypart.Day,
"Дневной блок", DayBlock,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg, ShowAudience.Pg,
BlockMinutes: 120, BlockMinutes: 120,
@@ -565,7 +579,7 @@ public static class GridProfiles
At(21), At(21),
At(22), At(22),
Daypart.Prime, Daypart.Prime,
"Спокойной ночи", GoodNight,
ShowKind.Series, ShowKind.Series,
ShowAudience.Pg, ShowAudience.Pg,
BlockMinutes: 60, BlockMinutes: 60,
@@ -575,7 +589,7 @@ public static class GridProfiles
At(22), At(22),
At(6), At(6),
Daypart.Night, Daypart.Night,
"Конец вещания", SignOffTitle,
SlotKind: SlotKind.SignOff, SlotKind: SlotKind.SignOff,
BlockMinutes: 0 BlockMinutes: 0
), ),
@@ -38,9 +38,14 @@ public sealed class RestoreTemplateCommandHandler(IAppDbContext dbContext)
// Слои, которых в снимке нет, — заведены после применения. Фоновый не трогаем: он есть // Слои, которых в снимке нет, — заведены после применения. Фоновый не трогаем: он есть
// в любом шаблоне и удалению не подлежит. // в любом шаблоне и удалению не подлежит.
foreach (var layer in template.Layers.ToList()) var staleLayers = template
if (!layer.IsBackground && snapshot.Layers.All(l => l.Id != layer.Id)) .Layers.Where(layer =>
template.RemoveLayer(layer.Id); !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) foreach (var layerSnapshot in snapshot.Layers)
RestoreLayer(template, layerSnapshot, refs); RestoreLayer(template, layerSnapshot, refs);
@@ -76,9 +81,12 @@ public sealed class RestoreTemplateCommandHandler(IAppDbContext dbContext)
snapshot.IsEnabled snapshot.IsEnabled
); );
foreach (var slot in layer.Slots.ToList()) var staleSlots = layer
if (snapshot.Slots.All(s => s.Id != slot.Id)) .Slots.Where(slot => snapshot.Slots.All(s => s.Id != slot.Id))
layer.RemoveSlot(slot.Id); .Select(slot => slot.Id)
.ToList();
foreach (var slotId in staleSlots)
layer.RemoveSlot(slotId);
foreach (var slotSnapshot in snapshot.Slots) foreach (var slotSnapshot in snapshot.Slots)
RestoreSlot(layer, slotSnapshot, refs); RestoreSlot(layer, slotSnapshot, refs);
@@ -212,9 +212,9 @@ public sealed class ValidateTemplateQueryHandler(
{ {
var slots = layers.SelectMany(l => l.Slots).ToList(); var slots = layers.SelectMany(l => l.Slots).ToList();
foreach (var weekday in GridCoverage.Week) return from weekday in GridCoverage.Week
foreach (var (from, to) in GridCoverage.Gaps(slots, weekday, dayStart)) from gap in GridCoverage.Gaps(slots, weekday, dayStart)
yield return Gap(weekday, from, to, dayStart); select Gap(weekday, gap.From, gap.To, dayStart);
} }
private static TemplateIssueDto Gap(int weekday, int from, int to, TimeOnly dayStart) private static TemplateIssueDto Gap(int weekday, int from, int to, TimeOnly dayStart)
@@ -152,10 +152,13 @@ public sealed class FileSystemMediaStorage(MediaPathResolver paths) : IMediaStor
private static void DeleteCompanions(string directory, CompanionMatcher companion) 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 try
{ {
File.Delete(file); File.Delete(file);