From e55161f8f94873576076785abc7971a542f1f51c Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Mon, 27 Jul 2026 19:53:01 +0300 Subject: [PATCH] Enhance GridPlanner with SlotFactory for improved slot composition and management Refactored the GridPlanner class to introduce a SlotFactory for better organization and clarity in slot composition. Added detailed summaries for methods to improve code documentation and understanding. Enhanced the logic for handling content and repeat slots, ensuring more efficient management of grid slots during generation. These changes contribute to improved readability and maintainability of the code. --- .../Templates/Generate/GridPlanner.cs | 106 +++++++++++------- .../src/features/admin/media/episode-parse.ts | 8 +- 2 files changed, 68 insertions(+), 46 deletions(-) diff --git a/backend/src/TeleWave.Application/Programming/Templates/Generate/GridPlanner.cs b/backend/src/TeleWave.Application/Programming/Templates/Generate/GridPlanner.cs index 41d1694..5e93f43 100644 --- a/backend/src/TeleWave.Application/Programming/Templates/Generate/GridPlanner.cs +++ b/backend/src/TeleWave.Application/Programming/Templates/Generate/GridPlanner.cs @@ -306,6 +306,13 @@ public sealed class GridPlanner( } } + /// + /// Чем закрыть точку сетки. Решение здесь, сборка — в : «что ставим» + /// и «как это собрать» читаются по отдельности. + /// + /// Порядок предпочтений: свежий контент → повтор вечернего блока → контент по второму кругу → + /// ничего (место закроет фон). + /// private static PlannedSlot? Compose( GridBand band, SlotTarget target, @@ -315,73 +322,79 @@ public sealed class GridPlanner( PlanRun run ) { + var factory = new SlotFactory(band, target, offset, anchor, run); + if (band.SlotKind == SlotKind.SignOff) - return Build(band.Title, null, available, SlotBlockMode.FillSlot, 1, null, null); + return factory.SignOff(available); if (band.SlotKind == SlotKind.Repeat) - return RepeatSlot(band.Title, band.RepeatFrom, available); + return factory.Repeat(band.RepeatFrom, available); var group = Pick(band, target, run, available); + if (group is not null && !run.Exhausted(group)) + return factory.Content(group, available); - // Ставить нечего — либо подходящей группы нет вовсе, либо свежий контент на неделю кончился. - // И то и другое закрывается повтором вечернего блока, как на настоящем ТВ: крутить по - // третьему кругу одно и то же хуже — зритель видит то же самое, но без подписи «повтор». - if (group is null || run.Exhausted(group)) + // Свежего контента нет — либо подходящей группы не нашлось вовсе, либо ёмкость на неделю + // исчерпана. И то и другое закрывается повтором, как на настоящем ТВ: крутить по третьему + // кругу одно и то же хуже — зритель видит то же самое, но без честной подписи «повтор». + if (CanRepeat(band, run)) { - if (!CanRepeat(band, run)) - { - run.Note(Unfillable(band, group), isError: group is null); - return group is null ? null : ContentSlot(group); - } - - run.Note( - group is null - ? $"Для полосы «{band.Title}» подходящей группы нет — её закрывает повтор " - + "вечернего блока." - : $"Полосу «{band.Title}» закрывает повтор вечернего блока: свежего контента " - + "на неделю не хватает." - ); - return RepeatSlot( - band.Title, - run.PrimeStart!.Value, - Math.Min(available, RepeatMinutes(band)) - ); + run.Note(RepeatReason(band, group)); + return factory.Repeat(run.PrimeStart!.Value, Math.Min(available, RepeatMinutes(band))); } - return ContentSlot(group); + run.Note(Unfillable(band, group), isError: group is null); + return group is null ? null : factory.Content(group, available); + } - PlannedSlot ContentSlot(GroupCandidate picked) + /// + /// Сборка слота в известной точке сетки: полоса, день, слой и смещение уже определены, меняется + /// только наполнение. Отдельный тип, а не набор замыканий внутри : там + /// принимается решение, здесь — оформляется. + /// + private readonly record struct SlotFactory( + GridBand Band, + SlotTarget Target, + int Offset, + bool Anchor, + PlanRun Run + ) + { + public PlannedSlot Content(GroupCandidate group, int available) { - var (duration, units) = Measure(band, picked, available); - run.Reserve(picked, units, duration, target.AiringsPerWeek); + var (duration, units) = Measure(Band, group, available); + Run.Reserve(group, units, duration, Target.AiringsPerWeek); return Build( - picked.Name, - picked.Id, + group.Name, + group.Id, duration, - band.UnitsPerBlock > 0 ? SlotBlockMode.Count : SlotBlockMode.FillSlot, + Band.UnitsPerBlock > 0 ? SlotBlockMode.Count : SlotBlockMode.FillSlot, Math.Max(1, units), new SlotStrategy( - band.Strategy, + Band.Strategy, RestartOnEnd: true, - CooldownDays: band.CooldownDays + CooldownDays: Band.CooldownDays ), null ); } - PlannedSlot RepeatSlot(string title, TimeOnly repeatFrom, int minutes) => + public PlannedSlot Repeat(TimeOnly from, int minutes) => Build( - title, + Band.Title, null, minutes, SlotBlockMode.FillSlot, 1, null, - new RepeatSource(band.RepeatDaysAgo, repeatFrom, minutes) + new RepeatSource(Band.RepeatDaysAgo, from, minutes) ); - PlannedSlot Build( + public PlannedSlot SignOff(int minutes) => + Build(Band.Title, null, minutes, SlotBlockMode.FillSlot, 1, null, null); + + private PlannedSlot Build( string title, Guid? groupId, int duration, @@ -391,23 +404,30 @@ public sealed class GridPlanner( RepeatSource? repeat ) => new( - target.Layer, - target.Weekday, - GridCoverage.AtOffset(offset, run.DayStart), + Target.Layer, + Target.Weekday, + GridCoverage.AtOffset(Offset, Run.DayStart), duration, title, - band.Daypart, - repeat is null ? band.SlotKind : SlotKind.Repeat, + Band.Daypart, + repeat is null ? Band.SlotKind : SlotKind.Repeat, groupId, groupId is null ? null : title, blockMode, blockValue, strategy, repeat, - anchor + Anchor ); } + /// Почему полосу закрыл повтор — текст для предпросмотра. + private static string RepeatReason(GridBand band, GroupCandidate? group) => + group is null + ? $"Для полосы «{band.Title}» подходящей группы нет — её закрывает повтор вечернего блока." + : $"Полосу «{band.Title}» закрывает повтор вечернего блока: свежего контента на неделю " + + "не хватает."; + /// Почему полоса осталась без свежего контента — текст для предпросмотра. private static string Unfillable(GridBand band, GroupCandidate? group) => group is null diff --git a/frontend/src/features/admin/media/episode-parse.ts b/frontend/src/features/admin/media/episode-parse.ts index 3c4ff75..a5a7c51 100644 --- a/frontend/src/features/admin/media/episode-parse.ts +++ b/frontend/src/features/admin/media/episode-parse.ts @@ -77,15 +77,17 @@ export function parseEpisodeName(name: string, opts?: ParseOptions): ParsedEpiso // и движок перебирает варианты впустую (у Sonar это «super-linear backtracking»). const SEASON_RANGES = [ /(?:s|season|сезоны?)[\s._-]*\d{1,2}[\s._]*[-–][\s._]*(?:s|season)?\d{1,2}/i, - /\d{1,2}[\s._]*[-–][\s._]*\d{1,2}[\s._-]*сезон/i, + /\d{1,2}[\s._]*[-–][\s._]*\d{1,2}[\s._-]*(?:season|сезон)/i, ] -/** Сезон в имени одного сегмента пути: «S02», «Season 2», «Сезон 2», «2 сезон». */ +/** Сезон в имени одного сегмента пути: «S02», «Season 2», «Сезон 2», «2 сезон», «3 Season». */ const SEASON_PATTERNS = [ // S02 — не часть более длинного числа (год «S2007» сезоном не считаем). /(?:^|[^a-z0-9])s[\s._-]*(\d{1,2})(?!\d)/i, /(?:season|сезон)[\s._-]*(\d{1,2})(?!\d)/i, - /(\d{1,2})[\s._-]*сезон/i, + // Номер перед словом — «3 Season», «2 сезон». Ограничение слева не даёт принять за сезон + // хвост года: в «2007 Season» это не третий сезон, а сезон 2007 года. + /(?:^|[^\d])(\d{1,2})[\s._-]*(?:season|сезон)/i, ] /**