129 lines
5.4 KiB
C#
129 lines
5.4 KiB
C#
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> bumperOptions,
|
|
IOptions<StreamingOptions> streamingOptions
|
|
) : ICommandHandler<RenderBumperPreviewCommand, Result>
|
|
{
|
|
private readonly BumperOptions _bumper = bumperOptions.Value;
|
|
private readonly int _segmentSeconds = Math.Max(1, streamingOptions.Value.SegmentSeconds);
|
|
|
|
/// <summary>Длительность заставки без загруженного звука (сек) — как в генераторе.</summary>
|
|
private const int DefaultBumperDurationSeconds = 8;
|
|
|
|
public async Task<Result> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Примерные названия «из/в» для превью. Берём шоу из групп, на которые ссылаются слоты канала:
|
|
/// так превью показывает реальные названия этого канала, а не случайные из библиотеки.
|
|
/// </summary>
|
|
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) ?? "Второе шоу"
|
|
);
|
|
}
|
|
}
|