Refactor .gitignore to streamline ignored files and enhance clarity. Update CLAUDE.md to improve unit test instructions and add coverage reporting details. Revise README.md for better project overview and deployment instructions. Refactor ChannelEndpoints and StreamingEndpoints to utilize SegmentFiles for file resolution, improving code maintainability. Remove unused JunctionHandlers and update DependencyInjection for cleaner service registration. Enhance media processing services for better job handling and error management. Update frontend API types for consistency and clarity.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Application.Broadcast.Scheduling;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Streaming;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
|
||||
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||
|
||||
/// <summary>
|
||||
/// Восстанавливает <see cref="BumperRenderSpec"/> по кэш-строке заставки: планировщик сохранил только
|
||||
/// ссылки (канал/блок/подблок/пара шоу), а рендеру нужны названия шоу и абсолютные пути к звуку,
|
||||
/// постеру и фону. Вынесено из фонового рендерера: чтение и сборка — работа слоя приложения,
|
||||
/// воркер лишь крутит ffmpeg.
|
||||
/// </summary>
|
||||
public sealed class BumperSpecLoader(
|
||||
IAppDbContext dbContext,
|
||||
IBumperTemplateStorage bumperStorage,
|
||||
IImageStore imageStore,
|
||||
IOptions<BumperOptions> bumperOptions,
|
||||
IOptions<StreamingOptions> streamingOptions
|
||||
)
|
||||
{
|
||||
private readonly BumperOptions _bumper = bumperOptions.Value;
|
||||
private readonly int _segmentSeconds = Math.Max(1, streamingOptions.Value.SegmentSeconds);
|
||||
|
||||
/// <summary>Спецификация заставки для ассета, либо null если восстановить её уже нельзя.</summary>
|
||||
public async Task<BumperRenderSpec?> LoadAsync(
|
||||
Guid assetId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var cache = await dbContext
|
||||
.BumperAssets.AsNoTracking()
|
||||
.Where(b => b.MediaAssetId == assetId)
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (cache is null)
|
||||
return null;
|
||||
|
||||
var channel = await dbContext
|
||||
.Channels.AsNoTracking()
|
||||
.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.FirstOrDefaultAsync(c => c.Id == cache.ChannelId, cancellationToken);
|
||||
var template = channel?.BumperTemplates.FirstOrDefault(t => t.Id == cache.TemplateId);
|
||||
var variant = template?.Variants.FirstOrDefault(v => v.Id == cache.VariantId);
|
||||
if (channel is null || template is null || variant is null)
|
||||
return null;
|
||||
|
||||
var names = await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => s.Id == cache.FromShowId || s.Id == cache.ToShowId)
|
||||
.Select(s => new { s.Id, s.Name })
|
||||
.ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
|
||||
|
||||
// Постер шоу-получателя как фон — только для «Сейчас/Далее».
|
||||
string? posterPath = null;
|
||||
if (variant.Kind == BumperTextKind.NowNext)
|
||||
posterPath = await ResolveShowPosterAsync(cache.ToShowId, cancellationToken);
|
||||
|
||||
var bgPath = await ResolveImagePathAsync(template.BackgroundImageId, cancellationToken);
|
||||
var aligned = BumperDuration.Aligned(
|
||||
BumperDuration.TemplateSeconds(template),
|
||||
_segmentSeconds
|
||||
);
|
||||
|
||||
return BumperSpecFactory.Build(
|
||||
_bumper,
|
||||
channel.BumperFont,
|
||||
template,
|
||||
variant,
|
||||
aligned,
|
||||
names.GetValueOrDefault(cache.FromShowId, "…"),
|
||||
names.GetValueOrDefault(cache.ToShowId, "…"),
|
||||
bumperStorage.AudioPath(template.Id, template.AudioExtension),
|
||||
posterPath,
|
||||
bgPath
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<string?> ResolveShowPosterAsync(
|
||||
Guid showId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var posterImageId = await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => s.Id == showId && s.PosterImageId != null)
|
||||
.Select(s => s.PosterImageId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return await ResolveImagePathAsync(posterImageId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<string?> ResolveImagePathAsync(
|
||||
Guid? imageId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (imageId is not { } id)
|
||||
return null;
|
||||
|
||||
var ext = await dbContext
|
||||
.Images.AsNoTracking()
|
||||
.Where(i => i.Id == id)
|
||||
.Select(i => i.FileExtension)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return ext is null ? null : imageStore.ResolvePath(id, ext);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user