using LiveRecorder.Application.Abstractions.Persistence; using LiveRecorder.Application.Abstractions.Recording; using LiveRecorder.Application.Models.RecordTasks; using LiveRecorder.Domain.Entities; using LiveRecorder.Domain.Enums; namespace LiveRecorder.Application.Services; public sealed class TranscodeTaskService { private readonly IRecordTaskRepository _recordTaskRepository; private readonly IFfmpegService _ffmpegService; public TranscodeTaskService( IRecordTaskRepository recordTaskRepository, IFfmpegService ffmpegService) { _recordTaskRepository = recordTaskRepository; _ffmpegService = ffmpegService; } public async Task> ListAsync(CancellationToken cancellationToken = default) { var tasks = await _recordTaskRepository.ListAsync(null, cancellationToken); if (tasks.Count == 0) { return []; } var runtimeStates = _ffmpegService.GetTaskRuntimeStates(tasks.Select(static item => item.Id).ToArray()); return tasks .Select(task => { runtimeStates.TryGetValue(task.Id, out var runtimeState); var mappedTask = RecordModelMapper.MapTask(task, runtimeState); var sourceFilePath = ResolveManualTranscodeSourcePath(task); var canManualTranscode = CanManuallyTranscode(task, runtimeState, sourceFilePath); return new TranscodeTaskItemDto { Task = mappedTask, Result = task.Result is null ? null : RecordModelMapper.MapResult(task.Result), SourceFilePath = sourceFilePath, CanManualTranscode = canManualTranscode }; }) .Where(static item => ShouldInclude(item)) .OrderByDescending(static item => item.Task.CreatedAt) .ToArray(); } private static bool ShouldInclude(TranscodeTaskItemDto item) { if (!string.IsNullOrWhiteSpace(item.Task.PostProcessStage)) { return true; } if (item.CanManualTranscode) { return true; } return item.Task.OutputFormat == RecordOutputFormat.Mp4 && item.Task.Status == RecordTaskStatus.Processing; } private static bool CanManuallyTranscode( RecordTask recordTask, RecordTaskRuntimeState? runtimeState, string? sourceFilePath) { if (recordTask.OutputFormat != RecordOutputFormat.Mp4 || runtimeState is not null || IsActiveStatus(recordTask.Status)) { return false; } if (!string.IsNullOrWhiteSpace(sourceFilePath) && File.Exists(sourceFilePath)) { return true; } var resultPath = recordTask.Result?.FilePath?.ToLowerInvariant() ?? string.Empty; var errorText = $"{recordTask.ErrorMessage ?? string.Empty} {recordTask.Result?.ErrorMessage ?? string.Empty}".ToLowerInvariant(); return resultPath.EndsWith(".ts", StringComparison.OrdinalIgnoreCase) || errorText.Contains("finaliz", StringComparison.Ordinal) || errorText.Contains("intermediate ts", StringComparison.Ordinal); } private static string? ResolveManualTranscodeSourcePath(RecordTask recordTask) { if (!string.IsNullOrWhiteSpace(recordTask.Result?.FilePath) && recordTask.Result.FilePath.EndsWith(".ts", StringComparison.OrdinalIgnoreCase)) { return recordTask.Result.FilePath; } if (!string.IsNullOrWhiteSpace(recordTask.OutputFilePath)) { var normalizedOutput = NormalizeAbsolutePath(recordTask.OutputFilePath); if (normalizedOutput.EndsWith(".ts", StringComparison.OrdinalIgnoreCase)) { return normalizedOutput; } var candidate = Path.ChangeExtension(normalizedOutput, ".ts"); if (File.Exists(candidate)) { return candidate; } var singleFileCandidate = Path.Combine( Path.GetDirectoryName(normalizedOutput) ?? string.Empty, $"{Path.GetFileNameWithoutExtension(normalizedOutput)}.recording.ts"); if (File.Exists(singleFileCandidate)) { return singleFileCandidate; } } return null; } private static string NormalizeAbsolutePath(string path) => Path.IsPathRooted(path) ? path : Path.GetFullPath(path, AppContext.BaseDirectory); private static bool IsActiveStatus(RecordTaskStatus status) => status is RecordTaskStatus.Starting or RecordTaskStatus.Running or RecordTaskStatus.Stopping or RecordTaskStatus.Processing; }