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,