diff --git a/backend/Directory.Build.props b/backend/Directory.Build.props index 5a34a55..0c5da06 100644 --- a/backend/Directory.Build.props +++ b/backend/Directory.Build.props @@ -9,6 +9,18 @@ latest false true - $(NoWarn);CA1711;CA1716;CA1848;CA1873 + + $(NoWarn);CA1711;CA1716;CA1848;CA1873;CA1859;ASP0018 diff --git a/backend/src/TeleWave.Domain/Programming/Planning/JunctionFiller.cs b/backend/src/TeleWave.Domain/Programming/Planning/JunctionFiller.cs index c3e887e..026f26f 100644 --- a/backend/src/TeleWave.Domain/Programming/Planning/JunctionFiller.cs +++ b/backend/src/TeleWave.Domain/Programming/Planning/JunctionFiller.cs @@ -18,6 +18,19 @@ public sealed class JunctionHistory _lastPlaced[element.Kind] = moment; } +/// +/// Где ставится стык и между чем. Собрано в один параметр: по отдельности эти четыре значения ехали +/// сквозь всю раскладку и вместе с курсором, пределом и накопителями раздували сигнатуры. +/// +/// Шоу перед стыком, — после; заставке нужна +/// именно пара соседей, её ассет рендерится под неё после сборки ленты. +public sealed record JunctionPlacement( + Guid SlotId, + Guid? FromShowId, + Guid? ToShowId, + bool ElementChanged +); + /// /// Раскладка врезок стыка: реклама, промо, заставка, заполнитель. Ставит только то, что влезает /// целиком до предела (якорь или горизонт) — обрезать врезку нельзя, а перехлёст сдвинул бы якорь. @@ -27,19 +40,12 @@ public sealed class JunctionHistory /// public static class JunctionFiller { - /// - /// Раскладывает стык от . / - /// нужны заставке: её ассет зависит от пары соседей и рендерится после сборки ленты. - /// Возвращает курсор после стыка. - /// + /// Раскладывает стык от . Возвращает курсор после стыка. public static DateTimeOffset Fill( PlanningJunction? junction, DateTimeOffset cursor, DateTimeOffset limit, - Guid slotId, - Guid? fromShowId, - Guid? toShowId, - bool elementChanged, + JunctionPlacement placement, JunctionHistory history, List items, PlanTrace? trace @@ -55,7 +61,7 @@ public static class JunctionFiller foreach (var element in ordered) { - if (element.OnlyOnElementChange && !elementChanged) + if (element.OnlyOnElementChange && !placement.ElementChanged) continue; if (!history.Allows(element, cursor)) continue; @@ -63,8 +69,8 @@ public static class JunctionFiller var placedAt = cursor; cursor = element.Kind == JunctionElementKind.Bumper - ? PlaceBumper(element, cursor, limit, slotId, fromShowId, toShowId, items, trace) - : PlaceUnits(element, cursor, limit, slotId, items, trace); + ? PlaceBumper(element, cursor, limit, placement, items, trace) + : PlaceUnits(element, cursor, limit, placement.SlotId, items, trace); if (cursor > placedAt) history.Record(element, placedAt); @@ -81,9 +87,7 @@ public static class JunctionFiller PlanningJunctionElement element, DateTimeOffset cursor, DateTimeOffset limit, - Guid slotId, - Guid? fromShowId, - Guid? toShowId, + JunctionPlacement placement, List items, PlanTrace? trace ) @@ -97,14 +101,14 @@ public static class JunctionFiller Guid.Empty, cursor, end, - toShowId, + placement.ToShowId, null, - slotId, + placement.SlotId, PlannedItemKind.Bumper, trace, element.BumperTemplateId, - fromShowId, - toShowId + placement.FromShowId, + placement.ToShowId ) ); return end; diff --git a/backend/src/TeleWave.Domain/Programming/Planning/SchedulePlanner.cs b/backend/src/TeleWave.Domain/Programming/Planning/SchedulePlanner.cs index 2e2cfdd..1c3007f 100644 --- a/backend/src/TeleWave.Domain/Programming/Planning/SchedulePlanner.cs +++ b/backend/src/TeleWave.Domain/Programming/Planning/SchedulePlanner.cs @@ -14,20 +14,35 @@ public static class SchedulePlanner { private const int IterationBackstop = 100_000; + /// + /// Накопители одного прогона: собираемая лента, новые курсоры слотов, предупреждения, история + /// врезок и шоу последней поставленной единицы. Всё это протаскивалось через сигнатуры десятком + /// параметров (включая ref), хотя принадлежит прогону целиком, а не отдельному шагу. + /// + /// История врезок общая на прогон намеренно: «не чаще раза в полчаса» должно работать и через + /// границу слота. + /// + private sealed class PlanningRun(PlanningInput input, IRandomSource random) + { + public PlanningInput Input { get; } = input; + public IRandomSource Random { get; } = random; + public List Items { get; } = []; + public List Cursors { get; } = []; + public List Warnings { get; } = []; + public JunctionHistory Junctions { get; } = new(); + + /// Шоу последней поставленной единицы — по нему стык понимает, сменился ли элемент. + public Guid? PreviousShowId { get; set; } + } + public static PlanningResult Plan(PlanningInput input, IRandomSource random) { - var items = new List(); - var cursors = new List(); - var warnings = new List(); + var run = new PlanningRun(input, random); var slots = input.Slots.OrderBy(s => s.TargetStartUtc).ToList(); var cursor = input.StartUtc; var iterations = 0; - // История врезок общая на прогон: «не чаще раза в полчаса» должно работать и через границу слота. - var junctions = new JunctionHistory(); - Guid? previousShowId = null; - for (var i = 0; i < slots.Count && cursor < input.HorizonEndUtc; i++) { if (iterations++ > IterationBackstop) @@ -42,32 +57,20 @@ public static class SchedulePlanner var nextAnchor = FindNextAnchor(slots, i + 1); - cursor = OpenSlot(slot, cursor, input, items, warnings, out var trace); + cursor = OpenSlot(slot, cursor, run, out var trace); if (cursor >= input.HorizonEndUtc) break; - cursor = FillSlot( - slot, - cursor, - nextAnchor, - input, - random, - items, - cursors, - warnings, - trace, - junctions, - ref previousShowId - ); + cursor = FillSlot(slot, cursor, nextAnchor, trace, run); } // Хвост до горизонта закрывает фон: лента обязана быть непрерывной, иначе живой край // упрётся в дыру. if (cursor < input.HorizonEndUtc) - cursor = FillWithFallback(cursor, input.HorizonEndUtc, input, items, null); + cursor = FillWithFallback(cursor, input.HorizonEndUtc, run, null); if (cursor < input.HorizonEndUtc && input.FallbackUnits.Count == 0) - warnings.Add( + run.Warnings.Add( new PlanningWarning( PlanningWarningKind.FallbackEmpty, null, @@ -75,7 +78,7 @@ public static class SchedulePlanner ) ); - return new PlanningResult(items, cursors, warnings); + return new PlanningResult(run.Items, run.Cursors, run.Warnings); } /// @@ -85,9 +88,7 @@ public static class SchedulePlanner private static DateTimeOffset OpenSlot( PlanningSlot slot, DateTimeOffset cursor, - PlanningInput input, - List items, - List warnings, + PlanningRun run, out PlanTrace trace ) { @@ -97,7 +98,7 @@ public static class SchedulePlanner { // До целевого времени ещё есть место — закрываем его фоном. Для якоря это обязательно, // для обычного слота тоже: иначе он начнётся раньше объявленного в программе времени. - cursor = FillWithFallback(cursor, slot.TargetStartUtc, input, items, slot.SlotId); + cursor = FillWithFallback(cursor, slot.TargetStartUtc, run, slot.SlotId); } else if (slot.SnapToMinutes is { } snap && snap > 0) { @@ -106,7 +107,7 @@ public static class SchedulePlanner var target = RoundUp(cursor, TimeSpan.FromMinutes(snap)); if (target - cursor <= TimeSpan.FromMinutes(slot.MaxDriftMinutes)) { - var afterFill = FillWithFallback(cursor, target, input, items, slot.SlotId); + var afterFill = FillWithFallback(cursor, target, run, slot.SlotId); snapped = afterFill > cursor; cursor = afterFill; } @@ -114,7 +115,7 @@ public static class SchedulePlanner var drift = (int)Math.Round((cursor - slot.TargetStartUtc).TotalMinutes); if (Math.Abs(drift) > slot.MaxDriftMinutes) - warnings.Add( + run.Warnings.Add( new PlanningWarning( PlanningWarningKind.DriftExceeded, slot.SlotId, @@ -140,17 +141,11 @@ public static class SchedulePlanner PlanningSlot slot, DateTimeOffset cursor, DateTimeOffset? nextAnchor, - PlanningInput input, - IRandomSource random, - List items, - List cursors, - List warnings, PlanTrace trace, - JunctionHistory junctions, - ref Guid? previousShowId + PlanningRun run ) { - var limit = Min(input.HorizonEndUtc, nextAnchor); + var limit = Min(run.Input.HorizonEndUtc, nextAnchor); switch (slot.SlotKind) { @@ -159,30 +154,17 @@ public static class SchedulePlanner return FillWithFallback( cursor, Min(slot.TargetEndUtc, limit), - input, - items, + run, slot.SlotId, PlannedItemKind.SignOff, trace ); case SlotKind.Repeat: - return FillRepeat(slot, cursor, limit, input, items, warnings, trace); + return FillRepeat(slot, cursor, limit, trace, run); default: - return FillContent( - slot, - cursor, - limit, - input, - random, - items, - cursors, - warnings, - trace, - junctions, - ref previousShowId - ); + return FillContent(slot, cursor, limit, trace, run); } } @@ -190,29 +172,21 @@ public static class SchedulePlanner PlanningSlot slot, DateTimeOffset cursor, DateTimeOffset limit, - PlanningInput input, - List items, - List warnings, - PlanTrace trace + PlanTrace trace, + PlanningRun run ) { var units = slot.RepeatUnits ?? []; if (units.Count == 0) { - warnings.Add( + run.Warnings.Add( new PlanningWarning( PlanningWarningKind.RepeatSourceEmpty, slot.SlotId, "В источнике повтора ничего не нашлось — слот закрыт фоном." ) ); - return FillWithFallback( - cursor, - Min(slot.TargetEndUtc, limit), - input, - items, - slot.SlotId - ); + return FillWithFallback(cursor, Min(slot.TargetEndUtc, limit), run, slot.SlotId); } var slotEnd = Min(slot.TargetEndUtc, limit); @@ -220,7 +194,7 @@ public static class SchedulePlanner { if (cursor + unit.Duration > slotEnd) break; - items.Add(Program(unit, cursor, slot.SlotId, trace)); + run.Items.Add(Program(unit, cursor, slot.SlotId, trace)); cursor += unit.Duration; } @@ -231,31 +205,19 @@ public static class SchedulePlanner PlanningSlot slot, DateTimeOffset cursor, DateTimeOffset limit, - PlanningInput input, - IRandomSource random, - List items, - List cursors, - List warnings, PlanTrace trace, - JunctionHistory junctions, - ref Guid? previousShowId + PlanningRun run ) { - var pick = ElementSelector.Select(slot, cursor, random); + var pick = ElementSelector.Select(slot, cursor, run.Random); if (pick is null) { - warnings.Add(NoCandidatesWarning(slot)); - return FillWithFallback( - cursor, - Min(slot.TargetEndUtc, limit), - input, - items, - slot.SlotId - ); + run.Warnings.Add(NoCandidatesWarning(slot)); + return FillWithFallback(cursor, Min(slot.TargetEndUtc, limit), run, slot.SlotId); } if (pick.CooldownExhausted) - warnings.Add( + run.Warnings.Add( new PlanningWarning( PlanningWarningKind.CooldownExhausted, slot.SlotId, @@ -283,8 +245,8 @@ public static class SchedulePlanner && !FitsEntirely(element, unitIndex, cursor, budgetEnd) ) { - cursors.Add(CursorUpdate(slot, element, unitIndex)); - return FillWithFallback(cursor, budgetEnd, input, items, slot.SlotId); + run.Cursors.Add(CursorUpdate(slot, element, unitIndex)); + return FillWithFallback(cursor, budgetEnd, run, slot.SlotId); } while (unitIndex < element.Units.Count && cursor < limit) @@ -297,12 +259,14 @@ public static class SchedulePlanner slot.JunctionBetween, cursor, limit, - slot.SlotId, - previousShowId, - unit.ShowId, - elementChanged: previousShowId != unit.ShowId, - junctions, - items, + new JunctionPlacement( + slot.SlotId, + run.PreviousShowId, + unit.ShowId, + ElementChanged: run.PreviousShowId != unit.ShowId + ), + run.Junctions, + run.Items, slotTrace ); @@ -313,12 +277,12 @@ public static class SchedulePlanner if (!WithinBudget(slot, placed, accumulated, cursor, unit, budgetEnd)) break; - items.Add(Program(unit, cursor, slot.SlotId, slotTrace, CollectionOf(element))); + run.Items.Add(Program(unit, cursor, slot.SlotId, slotTrace, CollectionOf(element))); cursor += unit.Duration; accumulated += unit.Duration; unitIndex++; placed++; - previousShowId = unit.ShowId; + run.PreviousShowId = unit.ShowId; } // Врезки в конце блока ставятся до добора фоном: иначе реклама оказалась бы после заполнителя. @@ -327,20 +291,22 @@ public static class SchedulePlanner slot.JunctionAfter, cursor, limit, - slot.SlotId, - previousShowId, - null, - elementChanged: true, - junctions, - items, + new JunctionPlacement( + slot.SlotId, + run.PreviousShowId, + null, + ElementChanged: true + ), + run.Junctions, + run.Items, slotTrace ); - cursors.Add(CursorUpdate(slot, element, unitIndex)); + run.Cursors.Add(CursorUpdate(slot, element, unitIndex)); // Недобор до целевого конца закрываем фоном — только для слотов, чей бюджет привязан ко времени. if (slot.BlockMode == SlotBlockMode.FillSlot && cursor < budgetEnd) - cursor = FillWithFallback(cursor, budgetEnd, input, items, slot.SlotId); + cursor = FillWithFallback(cursor, budgetEnd, run, slot.SlotId); return cursor; } @@ -416,14 +382,14 @@ public static class SchedulePlanner private static DateTimeOffset FillWithFallback( DateTimeOffset from, DateTimeOffset until, - PlanningInput input, - List items, + PlanningRun run, Guid? slotId, PlannedItemKind kind = PlannedItemKind.Fallback, PlanTrace? trace = null ) { - if (input.FallbackUnits.Count == 0 || until <= from) + var fallback = run.Input.FallbackUnits; + if (fallback.Count == 0 || until <= from) return from; var cursor = from; @@ -432,13 +398,13 @@ public static class SchedulePlanner while (cursor < until && guard++ < IterationBackstop) { - var unit = input.FallbackUnits[index % input.FallbackUnits.Count]; + var unit = fallback[index % fallback.Count]; index++; if (unit.Duration <= TimeSpan.Zero || cursor + unit.Duration > until) break; - items.Add( + run.Items.Add( new PlannedItem( unit.MediaAssetId, cursor,