using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Streaming; public abstract class FFmpegProcessHandler : IRequestHandler> where T : FFmpegProcessRequest { private readonly IDbContextFactory _dbContextFactory; protected FFmpegProcessHandler(IDbContextFactory dbContextFactory) => _dbContextFactory = dbContextFactory; public async Task> Handle(T request, CancellationToken cancellationToken) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation> validation = await Validate(dbContext, request); return await validation.Match( tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2, tuple.Item3, cancellationToken), error => Task.FromResult>(error.Join())); } protected abstract Task> GetProcess( TvContext dbContext, T request, Channel channel, string ffmpegPath, string ffprobePath, CancellationToken cancellationToken); private static async Task>> Validate( TvContext dbContext, T request) => (await ChannelMustExist(dbContext, request), await FFmpegPathMustExist(dbContext), await FFprobePathMustExist(dbContext)) .Apply((channel, ffmpegPath, ffprobePath) => Tuple(channel, ffmpegPath, ffprobePath)); private static Task> ChannelMustExist(TvContext dbContext, T request) => dbContext.Channels .Include(c => c.FFmpegProfile) .ThenInclude(p => p.Resolution) .Include(c => c.Artwork) .Include(c => c.Watermark) .SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber) .MapT( channel => { channel.StreamingMode = request.Mode.ToLowerInvariant() switch { "hls-direct" => StreamingMode.HttpLiveStreamingDirect, "segmenter" => StreamingMode.HttpLiveStreamingSegmenter, "segmenter-v2" => StreamingMode.HttpLiveStreamingSegmenterV2, "ts" => StreamingMode.TransportStreamHybrid, "ts-legacy" => StreamingMode.TransportStream, _ => channel.StreamingMode }; return channel; }) .Map(o => o.ToValidation($"Channel number {request.ChannelNumber} does not exist.")); private static Task> FFmpegPathMustExist(TvContext dbContext) => dbContext.ConfigElements.GetValue(ConfigElementKey.FFmpegPath) .FilterT(File.Exists) .Map(maybePath => maybePath.ToValidation("FFmpeg path does not exist on filesystem")); private static Task> FFprobePathMustExist(TvContext dbContext) => dbContext.ConfigElements.GetValue(ConfigElementKey.FFprobePath) .FilterT(File.Exists) .Map(maybePath => maybePath.ToValidation("FFprobe path does not exist on filesystem")); }