Update scheduling parameters and refactor channel endpoints: extend HorizonDays to 7 and RetentionDays to 90 in appsettings.json. Consolidate channel-related endpoint logic by removing obsolete files and enhancing the ShowEndpoints with audience and genre management capabilities. Improve error handling and streamline command handlers for channel operations.
This commit is contained in:
+128
-123
@@ -1,123 +1,128 @@
|
||||
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;
|
||||
|
||||
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)
|
||||
.Include(c => c.Shows)
|
||||
.AsSplitQuery()
|
||||
.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 showIds = channel.Shows.Select(s => s.ShowId).Distinct().Take(2).ToList();
|
||||
var names =
|
||||
showIds.Count == 0
|
||||
? []
|
||||
: await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => showIds.Contains(s.Id))
|
||||
.Select(s => s.Name)
|
||||
.Take(2)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return (
|
||||
names.ElementAtOrDefault(0) ?? "Первое шоу",
|
||||
names.ElementAtOrDefault(1) ?? "Второе шоу"
|
||||
);
|
||||
}
|
||||
}
|
||||
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) ?? "Второе шоу"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user