From f895e1ddbce9ff7cbac659617f0845180209814f Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Tue, 28 Jul 2026 12:57:14 +0300 Subject: [PATCH] Refactor GridScheduleGenerator for improved channel handling and entry writing Simplified the logic for handling disabled channels by introducing the SilentChannelAsync method, which manages future entry deletions more efficiently. Refactored the entry writing process into a separate WriteEntries method to enhance code clarity and maintainability. These changes streamline the scheduling process and improve overall functionality in the GridScheduleGenerator class. --- .../Planning/GridScheduleGenerator.cs | 126 +++++++++++------- 1 file changed, 79 insertions(+), 47 deletions(-) diff --git a/backend/src/TeleWave.Application/Programming/Planning/GridScheduleGenerator.cs b/backend/src/TeleWave.Application/Programming/Planning/GridScheduleGenerator.cs index 0c74072..ad33cd6 100644 --- a/backend/src/TeleWave.Application/Programming/Planning/GridScheduleGenerator.cs +++ b/backend/src/TeleWave.Application/Programming/Planning/GridScheduleGenerator.cs @@ -64,20 +64,8 @@ public sealed class GridScheduleGenerator( if (channel is null) return new GenerationReport(0, [], ChannelSkipped: true); - // Канал выключен или без сетки — вещать нечем. По таймеру это просто пропуск, но по кнопке - // «Применить» админ ждёт, что лента придёт в соответствие с настройками: раз эфира нет, - // не должно остаться и будущих записей. Иначе они висят вечно — пересобрать их нечем, - // а удалить шоу, которое в них стоит, невозможно. if (!channel.IsEnabled || channel.TemplateId is null) - { - if (!rebuildFuture) - return new GenerationReport(0, [], ChannelSkipped: true); - - await dbContext - .ScheduleEntries.Where(e => e.ChannelId == channelId && e.StartsAtUtc >= now) - .ExecuteDeleteAsync(cancellationToken); - return new GenerationReport(0, []); - } + return await SilentChannelAsync(channelId, now, rebuildFuture, cancellationToken); await using var transaction = await dbContext.BeginTransactionAsync(cancellationToken); await dbContext.AcquireChannelLockAsync(channelId, cancellationToken); @@ -93,9 +81,7 @@ public sealed class GridScheduleGenerator( await CleanupAsync(channelId, now, cancellationToken); if (rebuildFuture) - await dbContext - .ScheduleEntries.Where(e => e.ChannelId == channelId && e.StartsAtUtc >= now) - .ExecuteDeleteAsync(cancellationToken); + await DropFutureAsync(channelId, now, cancellationToken); // Точка продолжения — конец последней сохранённой записи. При пересборке будущее уже удалено, // поэтому она укажет на границу неизменяемого прошлого. @@ -139,37 +125,7 @@ public sealed class GridScheduleGenerator( cancellationToken ); - var added = 0; - for (var index = 0; index < result.Items.Count; index++) - { - var item = result.Items[index]; - var assetId = item.MediaAssetId; - if ( - item.Kind == PlannedItemKind.Bumper - && !bumperAssets.TryGetValue(index, out assetId) - ) - continue; // Без ассета запись стала бы дырой в ленте. - - dbContext.ScheduleEntries.Add( - ScheduleEntry.FromSlot( - channel.Id, - assetId, - ToEntryKind(item.Kind), - item.StartsAtUtc, - item.EndsAtUtc, - new ScheduleEntryOrigin( - item.ShowId, - item.UnitIndex, - item.SlotId, - item.Trace is null - ? null - : JsonSerializer.Serialize(item.Trace, TraceJsonOptions), - item.CollectionId - ) - ) - ); - added++; - } + var added = WriteEntries(channel.Id, result.Items, bumperAssets); await SaveCursorsAsync(result.Cursors, cancellationToken); @@ -231,6 +187,82 @@ public sealed class GridScheduleGenerator( /// Чистит прошлое сверх окна хранения. Окно должно покрывать самое долгое остывание среди правил — /// история показов берётся из самой ленты, отдельного журнала нет. /// + /// + /// Канал выключен или без сетки — вещать нечем. По таймеру это просто пропуск, но по кнопке + /// «Применить» админ ждёт, что лента придёт в соответствие с настройками: раз эфира нет, + /// не должно остаться и будущих записей. Иначе они висят вечно — пересобрать их нечем, + /// а удалить шоу, которое в них стоит, невозможно. + /// + private async Task SilentChannelAsync( + Guid channelId, + DateTimeOffset now, + bool rebuildFuture, + CancellationToken cancellationToken + ) + { + if (!rebuildFuture) + return new GenerationReport(0, [], ChannelSkipped: true); + + await DropFutureAsync(channelId, now, cancellationToken); + return new GenerationReport(0, []); + } + + /// Снимает будущий хвост ленты. Прошлое и идущую запись не трогает никогда. + private Task DropFutureAsync( + Guid channelId, + DateTimeOffset now, + CancellationToken cancellationToken + ) => + dbContext + .ScheduleEntries.Where(e => e.ChannelId == channelId && e.StartsAtUtc >= now) + .ExecuteDeleteAsync(cancellationToken); + + /// + /// Переносит собранную ленту в записи расписания. Заставка без отрендеренного ассета + /// пропускается: запись без файла стала бы дырой в эфире. + /// + private int WriteEntries( + Guid channelId, + IReadOnlyList items, + IReadOnlyDictionary bumperAssets + ) + { + var added = 0; + + for (var index = 0; index < items.Count; index++) + { + var item = items[index]; + var assetId = item.MediaAssetId; + if ( + item.Kind == PlannedItemKind.Bumper + && !bumperAssets.TryGetValue(index, out assetId) + ) + continue; + + dbContext.ScheduleEntries.Add( + ScheduleEntry.FromSlot( + channelId, + assetId, + ToEntryKind(item.Kind), + item.StartsAtUtc, + item.EndsAtUtc, + new ScheduleEntryOrigin( + item.ShowId, + item.UnitIndex, + item.SlotId, + item.Trace is null + ? null + : JsonSerializer.Serialize(item.Trace, TraceJsonOptions), + item.CollectionId + ) + ) + ); + added++; + } + + return added; + } + private Task CleanupAsync( Guid channelId, DateTimeOffset now,