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,