using LiteCqrs; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Common.Models; using TeleWave.Application.Streaming; using TeleWave.Domain.Broadcast; using TeleWave.Domain.Programming; namespace TeleWave.Application.Broadcast.Bumpers; public sealed class RenderBumperPreviewCommandHandler( IAppDbContext dbContext, IBumperRenderer renderer, IBumperTemplateStorage storage, IImageStore imageStore, IOptions bumperOptions, IOptions streamingOptions ) : ICommandHandler { private readonly BumperOptions _bumper = bumperOptions.Value; private readonly int _segmentSeconds = Math.Max(1, streamingOptions.Value.SegmentSeconds); /// Длительность заставки без загруженного звука (сек) — как в генераторе. private const int DefaultBumperDurationSeconds = 8; public async Task Handle( RenderBumperPreviewCommand query, CancellationToken cancellationToken ) { var channel = await dbContext .Channels.AsNoTracking() .Include(c => c.BumperTemplates) .ThenInclude(t => t.Variants) .FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken); if (channel is null) return Result.Failure(ChannelErrors.NotFound); var template = channel.BumperTemplates.FirstOrDefault(t => t.Id == query.TemplateId); if (template is null) return Result.Failure(ChannelErrors.BumperTemplateNotFound); var (fromName, toName) = await SampleNamesAsync(channel, cancellationToken); var fontFile = channel.BumperFont == BumperFont.Serif ? _bumper.FontFileSerif : _bumper.FontFileSans; // Фон блока — из общего реестра по id (общий для всех подблоков). string? backgroundPath = null; if (template.BackgroundImageId is { } bgId) { var bgExt = await dbContext .Images.AsNoTracking() .Where(i => i.Id == bgId) .Select(i => i.FileExtension) .FirstOrDefaultAsync(cancellationToken); if (bgExt is not null) backgroundPath = imageStore.ResolvePath(bgId, bgExt); } var seconds = template.AudioDurationSeconds is { } d and > 0 ? d : DefaultBumperDurationSeconds; var aligned = (int)( Math.Ceiling(Math.Max(_segmentSeconds, seconds) / _segmentSeconds) * _segmentSeconds ); var audioPath = storage.AudioPath(template.Id, template.AudioExtension); // Рендерим каждый подблок в свой ассет-превью (id по подблоку). foreach (var variant in template.Variants.OrderBy(v => v.Position)) { var free = variant.Kind == BumperTextKind.Free; var spec = new BumperRenderSpec( aligned, _bumper.Width, _bumper.Height, template.BackgroundColor, template.BackgroundColor2, template.AccentColor, template.TextColor, fontFile, free ? "" : variant.NowLabel, free ? "" : fromName, free ? "" : variant.NextLabel, free ? "" : toName, backgroundPath, audioPath, // Постер зависит от конкретного «следующего» шоу — в превью не подставляем. null, free, variant.Line1, variant.Line2 ); await renderer.RenderAsync(BumperPreview.AssetId(variant.Id), spec, cancellationToken); } return Result.Success(); } /// /// Примерные названия «из/в» для превью. Берём шоу из групп, на которые ссылаются слоты канала: /// так превью показывает реальные названия этого канала, а не случайные из библиотеки. /// private async Task<(string From, string To)> SampleNamesAsync( Channel channel, CancellationToken cancellationToken ) { var names = await ( from slot in dbContext.Slots.AsNoTracking() join layer in dbContext.GridLayers.AsNoTracking() on slot.LayerId equals layer.Id join item in dbContext.GroupItems.AsNoTracking() on slot.GroupId equals item.GroupId join show in dbContext.Shows.AsNoTracking() on item.ElementId equals show.Id where layer.TemplateId == channel.TemplateId && item.ElementKind == GroupElementKind.Show select show.Name ) .Distinct() .Take(2) .ToListAsync(cancellationToken); return ( names.ElementAtOrDefault(0) ?? "Первое шоу", names.ElementAtOrDefault(1) ?? "Второе шоу" ); } }