diff --git a/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs b/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs index c7ca03423..511c1d6b3 100644 --- a/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs +++ b/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs @@ -13,7 +13,7 @@ public class GetAllHealthCheckResultsHandler : IRequestHandler results = await _healthCheckService.PerformHealthChecks(); + List results = await _healthCheckService.PerformHealthChecks(cancellationToken); return results.Filter(r => r.Status != HealthCheckStatus.NotApplicable).ToList(); } } \ No newline at end of file diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs index 8469b89ac..1cf96e5a5 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs @@ -47,21 +47,20 @@ public class ScanLocalLibraryHandler : IRequestHandler> Handle( + Task> IRequestHandler>.Handle( ForceScanLocalLibrary request, - CancellationToken cancellationToken) => Handle(request); + CancellationToken cancellationToken) => Handle(request, cancellationToken); - public Task> Handle( + Task> IRequestHandler>.Handle( ScanLocalLibraryIfNeeded request, - CancellationToken cancellationToken) => Handle(request); + CancellationToken cancellationToken) => Handle(request, cancellationToken); - private Task> - Handle(IScanLocalLibrary request) => + private Task> Handle(IScanLocalLibrary request, CancellationToken cancellationToken) => Validate(request) - .MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalLibrary.Name)) + .MapT(parameters => PerformScan(parameters, cancellationToken).Map(_ => parameters.LocalLibrary.Name)) .Bind(v => v.ToEitherAsync()); - private async Task PerformScan(RequestParameters parameters) + private async Task PerformScan(RequestParameters parameters, CancellationToken cancellationToken) { (LocalLibrary localLibrary, string ffprobePath, string ffmpegPath, bool forceScan, int libraryRefreshInterval) = parameters; @@ -92,7 +91,8 @@ public class ScanLocalLibraryHandler : IRequestHandler> +public class StartFFmpegSessionHandler : IRequestHandler> { private readonly ILogger _logger; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IConfigElementRepository _configElementRepository; - private readonly IHlsPlaylistFilter _hlsPlaylistFilter; private readonly ILocalFileSystem _localFileSystem; public StartFFmpegSessionHandler( @@ -24,15 +23,13 @@ public class StartFFmpegSessionHandler : MediatR.IRequestHandler logger, IServiceScopeFactory serviceScopeFactory, IFFmpegSegmenterService ffmpegSegmenterService, - IConfigElementRepository configElementRepository, - IHlsPlaylistFilter hlsPlaylistFilter) + IConfigElementRepository configElementRepository) { _localFileSystem = localFileSystem; _logger = logger; _serviceScopeFactory = serviceScopeFactory; _ffmpegSegmenterService = ffmpegSegmenterService; _configElementRepository = configElementRepository; - _hlsPlaylistFilter = hlsPlaylistFilter; } public Task> Handle(StartFFmpegSession request, CancellationToken cancellationToken) => @@ -54,7 +51,7 @@ public class StartFFmpegSessionHandler : MediatR.IRequestHandler worker, (_, _) => worker); // fire and forget worker - _ = worker.Run(request.ChannelNumber, idleTimeout) + _ = worker.Run(request.ChannelNumber, idleTimeout, cancellationToken) .ContinueWith( _ => _ffmpegSegmenterService.SessionWorkers.TryRemove( request.ChannelNumber, diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 7060396fb..2ff314245 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -1,11 +1,15 @@ using System.Diagnostics; using System.Timers; using Bugsnag; +using CliWrap; +using CliWrap.Buffered; using ErsatzTV.Application.Channels; +using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -55,19 +59,27 @@ public class HlsSessionWorker : IHlsSessionWorker DateTimeOffset filterBefore, CancellationToken cancellationToken) { - Option maybeLines = await ReadPlaylistLines(cancellationToken); - return maybeLines.Map(input => _hlsPlaylistFilter.TrimPlaylist(PlaylistStart, filterBefore, input)); + await Slim.WaitAsync(cancellationToken); + try + { + Option maybeLines = await ReadPlaylistLines(cancellationToken); + return maybeLines.Map(input => _hlsPlaylistFilter.TrimPlaylist(PlaylistStart, filterBefore, input)); + } + finally + { + Slim.Release(); + } } - public async Task Run(string channelNumber, TimeSpan idleTimeout) + public async Task Run(string channelNumber, TimeSpan idleTimeout, CancellationToken incomingCancellationToken) { - var cts = new CancellationTokenSource(); + var cts = CancellationTokenSource.CreateLinkedTokenSource(incomingCancellationToken); void Cancel(object o, ElapsedEventArgs e) => cts.Cancel(); - + try { _channelNumber = channelNumber; - + lock (_sync) { _timer = new Timer(idleTimeout.TotalMilliseconds) { AutoReset = false }; @@ -77,9 +89,14 @@ public class HlsSessionWorker : IHlsSessionWorker CancellationToken cancellationToken = cts.Token; _logger.LogInformation("Starting HLS session for channel {Channel}", channelNumber); - + using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); + ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService(); + if (localFileSystem.ListFiles(Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber)).Any()) + { + _logger.LogError("Transcode folder is NOT empty!"); + } _targetFramerate = await mediator.Send( new GetChannelFramerate(channelNumber), @@ -187,36 +204,33 @@ public class HlsSessionWorker : IHlsSessionWorker { await TrimAndDelete(cancellationToken); - Process process = processModel.Process; + using Process process = processModel.Process; _logger.LogInformation( "ffmpeg hls arguments {FFmpegArguments}", string.Join(" ", process.StartInfo.ArgumentList)); - process.Start(); try { - await process.WaitForExitAsync(cancellationToken); - process.WaitForExit(); + await Cli.Wrap(process.StartInfo.FileName) + .WithArguments(process.StartInfo.ArgumentList) + .WithValidation(CommandResultValidation.None) + .ExecuteAsync(cancellationToken); } catch (TaskCanceledException) { _logger.LogInformation("Terminating HLS process for channel {Channel}", _channelNumber); - process.Kill(); - process.WaitForExit(); - return false; } _logger.LogInformation("HLS process has completed for channel {Channel}", _channelNumber); - _transcodedUntil = processModel.Until; } } catch (Exception ex) { _logger.LogError(ex, "Error transcoding channel {Channel}", _channelNumber); - + try { IClient client = scope.ServiceProvider.GetRequiredService(); @@ -239,49 +253,61 @@ public class HlsSessionWorker : IHlsSessionWorker private async Task TrimAndDelete(CancellationToken cancellationToken) { - Option maybeLines = await ReadPlaylistLines(cancellationToken); - foreach (string[] lines in maybeLines) + await Slim.WaitAsync(cancellationToken); + try { - // trim playlist and insert discontinuity before appending with new ffmpeg process - TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity( - _playlistStart, - DateTimeOffset.Now.AddMinutes(-1), - lines); - await WritePlaylist(trimResult.Playlist, cancellationToken); - - // delete old segments - var allSegments = Directory.GetFiles( - Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber), - "live*.ts") - .Map( - file => - { - string fileName = Path.GetFileName(file); - var sequenceNumber = int.Parse(fileName.Replace("live", string.Empty).Split('.')[0]); - return new Segment(file, sequenceNumber); - }) - .ToList(); - - var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList(); - // if (toDelete.Count > 0) - // { - // _logger.LogInformation( - // "Deleting HLS segments {Min} to {Max} (less than {StartSequence})", - // toDelete.Map(s => s.SequenceNumber).Min(), - // toDelete.Map(s => s.SequenceNumber).Max(), - // trimResult.Sequence); - // } - - foreach (Segment segment in toDelete) + + Option maybeLines = await ReadPlaylistLines(cancellationToken); + foreach (string[] lines in maybeLines) { - File.Delete(segment.File); - } + // trim playlist and insert discontinuity before appending with new ffmpeg process + TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity( + _playlistStart, + DateTimeOffset.Now.AddMinutes(-1), + lines); + await WritePlaylist(trimResult.Playlist, cancellationToken); + + // delete old segments + var allSegments = Directory.GetFiles( + Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber), + "live*.ts") + .Map( + file => + { + string fileName = Path.GetFileName(file); + var sequenceNumber = int.Parse(fileName.Replace("live", string.Empty).Split('.')[0]); + return new Segment(file, sequenceNumber); + }) + .ToList(); + + var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList(); + // if (toDelete.Count > 0) + // { + // _logger.LogInformation( + // "Deleting HLS segments {Min} to {Max} (less than {StartSequence})", + // toDelete.Map(s => s.SequenceNumber).Min(), + // toDelete.Map(s => s.SequenceNumber).Max(), + // trimResult.Sequence); + // } + + foreach (Segment segment in toDelete) + { + File.Delete(segment.File); + } - _playlistStart = trimResult.PlaylistStart; + _playlistStart = trimResult.PlaylistStart; + } + } + finally + { + Slim.Release(); } } - private static async Task GetPtsOffset(IMediator mediator, string channelNumber, CancellationToken cancellationToken) + private static async Task GetPtsOffset( + IMediator mediator, + string channelNumber, + CancellationToken cancellationToken) { await Slim.WaitAsync(cancellationToken); try @@ -315,41 +341,25 @@ public class HlsSessionWorker : IHlsSessionWorker private async Task> ReadPlaylistLines(CancellationToken cancellationToken) { - await Slim.WaitAsync(cancellationToken); - try - { - string fileName = PlaylistFileName(); - if (File.Exists(fileName)) - { - return await File.ReadAllLinesAsync(fileName, cancellationToken); - } - - return None; - } - finally + string fileName = PlaylistFileName(); + if (File.Exists(fileName)) { - Slim.Release(); + return await File.ReadAllLinesAsync(fileName, cancellationToken); } + + return None; } - + private async Task WritePlaylist(string playlist, CancellationToken cancellationToken) { - await Slim.WaitAsync(cancellationToken); - try - { - string fileName = PlaylistFileName(); - await File.WriteAllTextAsync(fileName, playlist, cancellationToken); - } - finally - { - Slim.Release(); - } + string fileName = PlaylistFileName(); + await File.WriteAllTextAsync(fileName, playlist, cancellationToken); } private string PlaylistFileName() => Path.Combine( FileSystemLayout.TranscodeFolder, _channelNumber, "live.m3u8"); - + private record Segment(string File, int SequenceNumber); } \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs b/ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs index d990cf9c6..c8aa980f4 100644 --- a/ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs @@ -19,7 +19,7 @@ public abstract class FFmpegProcessHandler : IRequestHandler> validation = await Validate(dbContext, request); return await validation.Match( - tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2), + tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2, cancellationToken), error => Task.FromResult>(error.Join())); } @@ -27,7 +27,8 @@ public abstract class FFmpegProcessHandler : IRequestHandler>> Validate( TvContext dbContext, diff --git a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs index ad1de73cf..3b04cf318 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs @@ -25,7 +25,8 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler(ConfigElementKey.FFmpegSaveReports) diff --git a/ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs index 59ac9e934..326002729 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs @@ -57,42 +57,42 @@ public class GetLastPtsDurationHandler : IRequestHandler replaceLine = s => + { + if (!string.IsNullOrWhiteSpace(s)) + { + lastLine = s.Trim(); + } + }; + + CommandResult probe = await Cli.Wrap(parameters.FFprobePath) .WithArguments(argumentList) .WithValidation(CommandResultValidation.None) - .ExecuteBufferedAsync(cancellationToken); + .WithStandardOutputPipe(PipeTarget.ToDelegate(replaceLine)) + .ExecuteAsync(cancellationToken); if (probe.ExitCode != 0) { return BaseError.New($"FFprobe at {parameters.FFprobePath} exited with code {probe.ExitCode}"); } - string output = probe.StandardOutput; - if (string.IsNullOrWhiteSpace(probe.StandardOutput)) - { - output = probe.StandardError; - } - try { - string[] lines = output.Split("\n"); - IEnumerable nonEmptyLines = lines.Filter(s => !string.IsNullOrWhiteSpace(s)).Map(l => l.Trim()); - return PtsAndDuration.From(nonEmptyLines.Last()); + return PtsAndDuration.From(lastLine); } catch (Exception ex) { _client.Notify(ex); - await SaveTroubleshootingData(parameters.ChannelNumber, output); + await SaveTroubleshootingData(parameters.ChannelNumber, lastLine); } } diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index bc0d4158f..26fa339b2 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -58,7 +58,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< TvContext dbContext, GetPlayoutItemProcessByChannelNumber request, Channel channel, - string ffmpegPath) + string ffmpegPath, + CancellationToken cancellationToken) { DateTimeOffset now = request.Now; @@ -129,7 +130,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< song, channel, maybeGlobalWatermark, - ffmpegPath); + ffmpegPath, + cancellationToken); } bool saveReports = await dbContext.ConfigElements diff --git a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs index 3a0e606a8..96c2452e9 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs @@ -25,7 +25,8 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler(ConfigElementKey.FFmpegSaveReports) diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index 1718165e6..f40e2f989 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs index 0edb35b21..dcc26e3ef 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs @@ -2,6 +2,7 @@ using System.Security.Cryptography; using System.Text; using Bugsnag; +using CliWrap; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.FFmpeg; @@ -187,8 +188,10 @@ public class TranscodingTests Watermark watermark, // [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration) - [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec, - [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration) + [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] + string profileCodec, + [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] + HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.QsvCodecs))] string profileCodec, @@ -213,9 +216,11 @@ public class TranscodingTests { string resolution = padding == Padding.WithPadding ? "1920x1060" : "1920x1080"; - string videoFilter = videoScanKind == VideoScanKind.Interlaced ? "-vf tinterlace=interleave_top,fieldorder=tff" : string.Empty; + string videoFilter = videoScanKind == VideoScanKind.Interlaced + ? "-vf tinterlace=interleave_top,fieldorder=tff" + : string.Empty; string flags = videoScanKind == VideoScanKind.Interlaced ? "-flags +ildct+ilme" : string.Empty; - + string args = $"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputFormat.Encoder} -shortest -pix_fmt {inputFormat.PixelFormat} -strict -2 {flags} {file}"; var p1 = new Process @@ -235,7 +240,7 @@ public class TranscodingTests } var imageCache = new Mock(); - + // always return the static watermark resource imageCache.Setup( ic => ic.GetPathForImage( @@ -269,11 +274,12 @@ public class TranscodingTests var metadataRepository = new Mock(); metadataRepository .Setup(r => r.UpdateLocalStatistics(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((_, version, _) => - { - version.MediaFiles = v.MediaFiles; - v = version; - }); + .Callback( + (_, version, _) => + { + version.MediaFiles = v.MediaFiles; + v = version; + }); var localStatisticsProvider = new LocalStatisticsProvider( metadataRepository.Object, @@ -345,7 +351,7 @@ public class TranscodingTests break; } - Process process = await service.ForPlayoutItem( + using Process process = await service.ForPlayoutItem( ExecutableName("ffmpeg"), false, new Channel(Guid.NewGuid()) @@ -376,13 +382,8 @@ public class TranscodingTests 0, None); - process.StartInfo.RedirectStandardError = true; - process.EnableRaisingEvents = true; - // Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}"); - process.Start().Should().BeTrue(); - string[] unsupportedMessages = { "No support for codec", @@ -390,41 +391,31 @@ public class TranscodingTests "Provided device doesn't support" }; - var errorBuffer = new StringBuilder(); - - process.ErrorDataReceived += (_, errorLine) => - { - string data = errorLine.Data ?? string.Empty; - errorBuffer.AppendLine(data); - }; - - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - // string error = await process.StandardError.ReadToEndAsync(); - + var sb = new StringBuilder(); + CommandResult result; var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30)); try { - await process.WaitForExitAsync(timeoutSignal.Token); - // ReSharper disable once MethodHasAsyncOverload - process.WaitForExit(); + result = await Cli.Wrap(process.StartInfo.FileName) + .WithArguments(process.StartInfo.ArgumentList) + .WithValidation(CommandResultValidation.None) + .WithStandardErrorPipe(PipeTarget.ToStringBuilder(sb)) + .ExecuteAsync(timeoutSignal.Token); } catch (OperationCanceledException) { - process.Kill(); - IEnumerable quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'"); Assert.Fail($"Transcode failure (timeout): ffmpeg {string.Join(" ", quotedArgs)}"); return; } - var error = errorBuffer.ToString(); - bool isUnsupported = unsupportedMessages.Any(error.Contains); + string error = sb.ToString(); + bool isUnsupported = unsupportedMessages.Any(error.Contains); if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported) { var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList(); - process.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}"); + result.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}"); Assert.Warn($"Unsupported on this hardware: ffmpeg {string.Join(" ", quotedArgs)}"); } else if (error.Contains("Impossible to convert between")) @@ -435,14 +426,14 @@ public class TranscodingTests else { var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList(); - process.ExitCode.Should().Be(0, errorBuffer + Environment.NewLine + string.Join(" ", quotedArgs)); - if (process.ExitCode == 0) + result.ExitCode.Should().Be(0, error + Environment.NewLine + string.Join(" ", quotedArgs)); + if (result.ExitCode == 0) { Console.WriteLine(string.Join(" ", quotedArgs)); } } } - + private static string GetStringSha256Hash(string text) { if (string.IsNullOrEmpty(text)) diff --git a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs index 4c6ec5ac5..ec5f64c6c 100644 --- a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs @@ -98,7 +98,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -141,7 +142,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -185,7 +187,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -233,7 +236,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -284,7 +288,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -335,7 +340,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -385,7 +391,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -429,7 +436,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -475,7 +483,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -515,7 +524,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -560,7 +570,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); @@ -590,7 +601,8 @@ public class MovieFolderScannerTests FFmpegPath, FFprobePath, 0, - 1); + 1, + CancellationToken.None); result.IsRight.Should().BeTrue(); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index d3579601a..44c9d5a40 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -301,7 +301,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService WatermarkLocation watermarkLocation, int horizontalMarginPercent, int verticalMarginPercent, - int watermarkWidthPercent) => + int watermarkWidthPercent, + CancellationToken cancellationToken) => _ffmpegProcessService.GenerateSongImage( ffmpegPath, subtitleFile, @@ -314,7 +315,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService watermarkLocation, horizontalMarginPercent, verticalMarginPercent, - watermarkWidthPercent); + watermarkWidthPercent, + cancellationToken); private Process GetProcess( string ffmpegPath, diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index 6a369dfc5..1fbc7d152 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Bugsnag; +using CliWrap; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.FFmpeg; @@ -331,7 +332,8 @@ public class FFmpegProcessService : IFFmpegProcessService WatermarkLocation watermarkLocation, int horizontalMarginPercent, int verticalMarginPercent, - int watermarkWidthPercent) + int watermarkWidthPercent, + CancellationToken cancellationToken) { try { @@ -404,8 +406,10 @@ public class FFmpegProcessService : IFFmpegProcessService "ffmpeg song arguments {FFmpegArguments}", string.Join(" ", process.StartInfo.ArgumentList)); - process.Start(); - await process.WaitForExitAsync(); + await Cli.Wrap(process.StartInfo.FileName) + .WithArguments(process.StartInfo.ArgumentList) + .WithValidation(CommandResultValidation.None) + .ExecuteAsync(cancellationToken); return outputFile; } diff --git a/ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs b/ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs index 258433126..31bb97419 100644 --- a/ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs +++ b/ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs @@ -30,7 +30,8 @@ public class SongVideoGenerator : ISongVideoGenerator Song song, Channel channel, Option maybeGlobalWatermark, - string ffmpegPath) + string ffmpegPath, + CancellationToken cancellationToken) { Option subtitleFile = None; @@ -224,7 +225,8 @@ public class SongVideoGenerator : ISongVideoGenerator watermarkLocation, HORIZONTAL_MARGIN_PERCENT, VERTICAL_MARGIN_PERCENT, - WATERMARK_WIDTH_PERCENT); + WATERMARK_WIDTH_PERCENT, + cancellationToken); foreach (string si in maybeSongImage.RightToSeq()) { diff --git a/ErsatzTV.Core/Health/IHealthCheck.cs b/ErsatzTV.Core/Health/IHealthCheck.cs index 1bf059f3e..9737bfc78 100644 --- a/ErsatzTV.Core/Health/IHealthCheck.cs +++ b/ErsatzTV.Core/Health/IHealthCheck.cs @@ -2,5 +2,5 @@ public interface IHealthCheck { - Task Check(); + Task Check(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ErsatzTV.Core/Health/IHealthCheckService.cs b/ErsatzTV.Core/Health/IHealthCheckService.cs index ad72d3a1b..7d78161fb 100644 --- a/ErsatzTV.Core/Health/IHealthCheckService.cs +++ b/ErsatzTV.Core/Health/IHealthCheckService.cs @@ -2,5 +2,5 @@ public interface IHealthCheckService { - Task> PerformHealthChecks(); + Task> PerformHealthChecks(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs index 3f2b94527..634800648 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs @@ -57,5 +57,6 @@ public interface IFFmpegProcessService WatermarkLocation watermarkLocation, int horizontalMarginPercent, int verticalMarginPercent, - int watermarkWidthPercent); + int watermarkWidthPercent, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/ISongVideoGenerator.cs b/ErsatzTV.Core/Interfaces/FFmpeg/ISongVideoGenerator.cs index 29b3bf2a8..18ee2bb9a 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/ISongVideoGenerator.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/ISongVideoGenerator.cs @@ -8,5 +8,6 @@ public interface ISongVideoGenerator Song song, Channel channel, Option maybeGlobalWatermark, - string ffmpegPath); + string ffmpegPath, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs index 7417afcfc..4419bf9f1 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs @@ -9,5 +9,6 @@ public interface IMovieFolderScanner string ffmpegPath, string ffprobePath, decimal progressMin, - decimal progressMax); + decimal progressMax, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs index ad41774df..febaca737 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs @@ -9,5 +9,6 @@ public interface IMusicVideoFolderScanner string ffmpegPath, string ffprobePath, decimal progressMin, - decimal progressMax); + decimal progressMax, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs index 8cdbf41a2..16cb08fa2 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs @@ -9,5 +9,6 @@ public interface ISongFolderScanner string ffprobePath, string ffmpegPath, decimal progressMin, - decimal progressMax); + decimal progressMax, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs index 8dc29981f..930017a51 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs @@ -9,5 +9,6 @@ public interface ITelevisionFolderScanner string ffmpegPath, string ffprobePath, decimal progressMin, - decimal progressMax); + decimal progressMax, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs index 46584c9ec..746be454f 100644 --- a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Bugsnag; +using CliWrap; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.FFmpeg; @@ -124,7 +125,8 @@ public abstract class LocalFolderScanner Domain.Metadata metadata, ArtworkKind artworkKind, Option ffmpegPath, - Option attachedPicIndex) + Option attachedPicIndex, + CancellationToken cancellationToken) { DateTime lastWriteTime = _localFileSystem.GetLastWriteTime(artworkFile); @@ -168,8 +170,11 @@ public abstract class LocalFolderScanner artworkFile, picIndex, tempName); - process.Start(); - await process.WaitForExitAsync(); + + await Cli.Wrap(process.StartInfo.FileName) + .WithArguments(process.StartInfo.ArgumentList) + .WithValidation(CommandResultValidation.None) + .ExecuteAsync(cancellationToken); return tempName; }, @@ -178,8 +183,11 @@ public abstract class LocalFolderScanner // no attached pic index means convert to png string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt); using Process process = ffmpegProcessService.ConvertToPng(path, artworkFile, tempName); - process.Start(); - await process.WaitForExitAsync(); + + await Cli.Wrap(process.StartInfo.FileName) + .WithArguments(process.StartInfo.ArgumentList) + .WithValidation(CommandResultValidation.None) + .ExecuteAsync(cancellationToken); return tempName; }); diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs index eae92fbaa..6acae0459 100644 --- a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -67,7 +67,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner string ffmpegPath, string ffprobePath, decimal progressMin, - decimal progressMax) + decimal progressMax, + CancellationToken cancellationToken) { decimal progressSpread = progressMax - progressMin; @@ -134,8 +135,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner .GetOrAdd(libraryPath, file) .BindT(movie => UpdateStatistics(movie, ffmpegPath, ffprobePath)) .BindT(UpdateMetadata) - .BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster)) - .BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt)) + .BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster, cancellationToken)) + .BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt, cancellationToken)) .BindT(FlagNormal); await maybeMovie.Match( @@ -229,7 +230,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner private async Task>> UpdateArtwork( MediaItemScanResult result, - ArtworkKind artworkKind) + ArtworkKind artworkKind, + CancellationToken cancellationToken) { try { @@ -238,7 +240,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner async posterFile => { MovieMetadata metadata = movie.MovieMetadata.Head(); - await RefreshArtwork(posterFile, metadata, artworkKind, None, None); + await RefreshArtwork(posterFile, metadata, artworkKind, None, None, cancellationToken); }); return result; diff --git a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs index c718c6c95..a2e8208bc 100644 --- a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs @@ -68,7 +68,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan string ffmpegPath, string ffprobePath, decimal progressMin, - decimal progressMax) + decimal progressMax, + CancellationToken cancellationToken) { decimal progressSpread = progressMax - progressMin; @@ -88,8 +89,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan Either> maybeArtist = await FindOrCreateArtist(libraryPath.Id, artistFolder) .BindT(artist => UpdateMetadataForArtist(artist, artistFolder)) - .BindT(artist => UpdateArtworkForArtist(artist, artistFolder, ArtworkKind.Thumbnail)) - .BindT(artist => UpdateArtworkForArtist(artist, artistFolder, ArtworkKind.FanArt)); + .BindT(artist => UpdateArtworkForArtist(artist, artistFolder, ArtworkKind.Thumbnail, cancellationToken)) + .BindT(artist => UpdateArtworkForArtist(artist, artistFolder, ArtworkKind.FanArt, cancellationToken)); await maybeArtist.Match( async result => @@ -99,7 +100,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan ffmpegPath, ffprobePath, result.Item, - artistFolder); + artistFolder, + cancellationToken); if (result.IsAdded) { @@ -212,7 +214,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan private async Task>> UpdateArtworkForArtist( MediaItemScanResult result, string artistFolder, - ArtworkKind artworkKind) + ArtworkKind artworkKind, + CancellationToken cancellationToken) { try { @@ -221,7 +224,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan async artworkFile => { ArtistMetadata metadata = artist.ArtistMetadata.Head(); - await RefreshArtwork(artworkFile, metadata, artworkKind, None, None); + await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); }); return result; @@ -238,7 +241,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan string ffmpegPath, string ffprobePath, Artist artist, - string artistFolder) + string artistFolder, + CancellationToken cancellationToken) { var folderQueue = new Queue(); folderQueue.Enqueue(artistFolder); @@ -277,7 +281,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan .GetOrAdd(artist, libraryPath, file) .BindT(musicVideo => UpdateStatistics(musicVideo, ffmpegPath, ffprobePath)) .BindT(UpdateMetadata) - .BindT(UpdateThumbnail) + .BindT(result => UpdateThumbnail(result, cancellationToken)) .BindT(FlagNormal); await maybeMusicVideo.Match( @@ -379,7 +383,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan } private async Task>> UpdateThumbnail( - MediaItemScanResult result) + MediaItemScanResult result, + CancellationToken cancellationToken) { try { @@ -388,7 +393,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan async thumbnailFile => { MusicVideoMetadata metadata = musicVideo.MusicVideoMetadata.Head(); - await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail, None, None); + await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail, None, None, cancellationToken); }); return result; diff --git a/ErsatzTV.Core/Metadata/SongFolderScanner.cs b/ErsatzTV.Core/Metadata/SongFolderScanner.cs index f5d28a44b..9d833130a 100644 --- a/ErsatzTV.Core/Metadata/SongFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/SongFolderScanner.cs @@ -66,7 +66,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner string ffprobePath, string ffmpegPath, decimal progressMin, - decimal progressMax) + decimal progressMax, + CancellationToken cancellationToken) { decimal progressSpread = progressMax - progressMin; @@ -130,7 +131,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner .GetOrAdd(libraryPath, file) .BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath)) .BindT(video => UpdateMetadata(video, ffprobePath)) - .BindT(video => UpdateThumbnail(video, ffmpegPath)) + .BindT(video => UpdateThumbnail(video, ffmpegPath, cancellationToken)) .BindT(FlagNormal); await maybeSong.Match( @@ -212,7 +213,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner private async Task>> UpdateThumbnail( MediaItemScanResult result, - string ffmpegPath) + string ffmpegPath, + CancellationToken cancellationToken) { try { @@ -234,10 +236,16 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner async thumbnailFile => { SongMetadata metadata = song.SongMetadata.Head(); - await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail, ffmpegPath, None); + await RefreshArtwork( + thumbnailFile, + metadata, + ArtworkKind.Thumbnail, + ffmpegPath, + None, + cancellationToken); }, - () => ExtractEmbeddedArtwork(song, ffmpegPath)); - + () => ExtractEmbeddedArtwork(song, ffmpegPath, cancellationToken)); + return result; } catch (Exception ex) @@ -246,7 +254,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner return BaseError.New(ex.ToString()); } } - + private Option LocateThumbnail(Song song) { string path = song.MediaVersions.Head().MediaFiles.Head().Path; @@ -263,7 +271,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner }).Flatten(); } - private async Task ExtractEmbeddedArtwork(Song song, string ffmpegPath) + private async Task ExtractEmbeddedArtwork(Song song, string ffmpegPath, CancellationToken cancellationToken) { Option maybeArtworkStream = Optional(song.GetHeadVersion().Streams.Find(ms => ms.AttachedPic)); foreach (MediaStream artworkStream in maybeArtworkStream) @@ -273,7 +281,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner song.SongMetadata.Head(), ArtworkKind.Thumbnail, ffmpegPath, - artworkStream.Index); + artworkStream.Index, + cancellationToken); } } } \ No newline at end of file diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index 403634e50..781ee75c1 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -67,7 +67,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan string ffmpegPath, string ffprobePath, decimal progressMin, - decimal progressMax) + decimal progressMax, + CancellationToken cancellationToken) { decimal progressSpread = progressMax - progressMin; @@ -85,9 +86,9 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan Either> maybeShow = await FindOrCreateShow(libraryPath.Id, showFolder) .BindT(show => UpdateMetadataForShow(show, showFolder)) - .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Poster)) - .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.FanArt)) - .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Thumbnail)); + .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Poster, cancellationToken)) + .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.FanArt, cancellationToken)) + .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Thumbnail, cancellationToken)); await maybeShow.Match( async result => @@ -97,7 +98,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan ffmpegPath, ffprobePath, result.Item, - showFolder); + showFolder, + cancellationToken); if (result.IsAdded) { @@ -159,7 +161,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan string ffmpegPath, string ffprobePath, Show show, - string showFolder) + string showFolder, + CancellationToken cancellationToken) { foreach (string seasonFolder in _localFileSystem.ListSubdirectories(showFolder).Filter(ShouldIncludeFolder) .OrderBy(identity)) @@ -182,12 +185,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan Either maybeSeason = await _televisionRepository .GetOrAddSeason(show, libraryPath.Id, seasonNumber) .BindT(EnsureMetadataExists) - .BindT(season => UpdatePoster(season, seasonFolder)); + .BindT(season => UpdatePoster(season, seasonFolder, cancellationToken)); await maybeSeason.Match( async season => { - await ScanEpisodes(libraryPath, ffmpegPath, ffprobePath, season, seasonFolder); + await ScanEpisodes(libraryPath, ffmpegPath, ffprobePath, season, seasonFolder, cancellationToken); await _libraryRepository.SetEtag(libraryPath, knownFolder, seasonFolder, etag); season.Show = show; @@ -212,7 +215,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan string ffmpegPath, string ffprobePath, Season season, - string seasonPath) + string seasonPath, + CancellationToken cancellationToken) { var allSeasonFiles = _localFileSystem.ListSubdirectories(seasonPath) .Map(_localFileSystem.ListFiles) @@ -232,7 +236,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan episode => UpdateStatistics(new MediaItemScanResult(episode), ffmpegPath, ffprobePath) .MapT(_ => episode)) .BindT(UpdateMetadata) - .BindT(UpdateThumbnail) + .BindT(e => UpdateThumbnail(e, cancellationToken)) .BindT(e => FlagNormal(new MediaItemScanResult(e))) .MapT(r => r.Item); @@ -363,7 +367,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan private async Task>> UpdateArtworkForShow( MediaItemScanResult result, string showFolder, - ArtworkKind artworkKind) + ArtworkKind artworkKind, + CancellationToken cancellationToken) { try { @@ -372,7 +377,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan async artworkFile => { ShowMetadata metadata = show.ShowMetadata.Head(); - await RefreshArtwork(artworkFile, metadata, artworkKind, None, None); + await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); }); return result; @@ -384,7 +389,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan } } - private async Task> UpdatePoster(Season season, string seasonFolder) + private async Task> UpdatePoster(Season season, string seasonFolder, CancellationToken cancellationToken) { try { @@ -392,7 +397,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan async posterFile => { SeasonMetadata metadata = season.SeasonMetadata.Head(); - await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster, None, None); + await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster, None, None, cancellationToken); }); return season; @@ -404,7 +409,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan } } - private async Task> UpdateThumbnail(Episode episode) + private async Task> UpdateThumbnail(Episode episode, CancellationToken cancellationToken) { try { @@ -413,7 +418,13 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan { foreach (EpisodeMetadata metadata in episode.EpisodeMetadata) { - await RefreshArtwork(posterFile, metadata, ArtworkKind.Thumbnail, None, None); + await RefreshArtwork( + posterFile, + metadata, + ArtworkKind.Thumbnail, + None, + None, + cancellationToken); } }); diff --git a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj index d97181210..2179482af 100644 --- a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj +++ b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj @@ -7,6 +7,7 @@ + diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index bb9e8716e..11e72515b 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -9,6 +9,7 @@ + diff --git a/ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs index 2c5e33533..80e9c5681 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs @@ -1,6 +1,6 @@ -using System.Diagnostics; +using CliWrap; +using CliWrap.Buffered; using ErsatzTV.Core.Health; -using Lucene.Net.Util; namespace ErsatzTV.Infrastructure.Health.Checks; @@ -28,27 +28,17 @@ public abstract class BaseHealthCheck protected HealthCheckResult InfoResult(string message) => new(Title, HealthCheckStatus.Info, message, None); - - protected static async Task GetProcessOutput(string path, IEnumerable arguments) + + protected static async Task GetProcessOutput( + string path, + IEnumerable arguments, + CancellationToken cancellationToken) { - var startInfo = new ProcessStartInfo - { - FileName = path, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - }; - - startInfo.ArgumentList.AddRange(arguments); - - var process = new Process - { - StartInfo = startInfo - }; - - process.Start(); - string result = await process.StandardOutput.ReadToEndAsync(); - await process.WaitForExitAsync(); - return result; + BufferedCommandResult result = await Cli.Wrap(path) + .WithArguments(arguments) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(cancellationToken); + + return result.StandardOutput; } } \ No newline at end of file diff --git a/ErsatzTV.Infrastructure/Health/Checks/EpisodeMetadataHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/EpisodeMetadataHealthCheck.cs index 63d423385..124d5f97e 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/EpisodeMetadataHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/EpisodeMetadataHealthCheck.cs @@ -14,16 +14,16 @@ public class EpisodeMetadataHealthCheck : BaseHealthCheck, IEpisodeMetadataHealt _dbContextFactory = dbContextFactory; protected override string Title => "Episode Metadata"; - - public async Task Check() + + public async Task Check(CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + List episodes = await dbContext.Episodes .Filter(e => e.EpisodeMetadata.Count == 0) .Include(e => e.MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); + .ToListAsync(cancellationToken); if (episodes.Any()) { @@ -36,7 +36,8 @@ public class EpisodeMetadataHealthCheck : BaseHealthCheck, IEpisodeMetadataHealt var folders = string.Join(", ", paths); - return WarningResult($"There are {episodes.Count} episodes with missing metadata, including in the following folders: {folders}"); + return WarningResult( + $"There are {episodes.Count} episodes with missing metadata, including in the following folders: {folders}"); } return OkResult(); diff --git a/ErsatzTV.Infrastructure/Health/Checks/ErrorReportsHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/ErrorReportsHealthCheck.cs index 8cb21355c..51b84d8be 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/ErrorReportsHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/ErrorReportsHealthCheck.cs @@ -16,7 +16,7 @@ public class ErrorReportsHealthCheck : BaseHealthCheck, IErrorReportsHealthCheck protected override string Title => "Error Reports"; - public Task Check() + public Task Check(CancellationToken cancellationToken) { if (_bugsnagConfiguration.Value.Enable) { @@ -26,6 +26,7 @@ public class ErrorReportsHealthCheck : BaseHealthCheck, IErrorReportsHealthCheck .AsTask(); } - return InfoResult("Automated error reporting is disabled. Please enable to support bug fixing efforts!").AsTask(); + return InfoResult("Automated error reporting is disabled. Please enable to support bug fixing efforts!") + .AsTask(); } } diff --git a/ErsatzTV.Infrastructure/Health/Checks/FFmpegReportsHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/FFmpegReportsHealthCheck.cs index 3f17c9e05..d727cd697 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/FFmpegReportsHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/FFmpegReportsHealthCheck.cs @@ -12,7 +12,7 @@ public class FFmpegReportsHealthCheck : BaseHealthCheck, IFFmpegReportsHealthChe public FFmpegReportsHealthCheck(IConfigElementRepository configElementRepository) => _configElementRepository = configElementRepository; - public async Task Check() + public async Task Check(CancellationToken cancellationToken) { Option saveReports = await _configElementRepository.GetValue(ConfigElementKey.FFmpegSaveReports); diff --git a/ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs index 784432b34..e8fbb76eb 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs @@ -16,7 +16,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe _configElementRepository = configElementRepository; } - public async Task Check() + public async Task Check(CancellationToken cancellationToken) { Option maybeFFmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath); if (maybeFFmpegPath.IsNone) @@ -29,9 +29,10 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe { return FailResult("Unable to locate ffprobe"); } + foreach (ConfigElement ffmpegPath in maybeFFmpegPath) { - Option maybeVersion = await GetVersion(ffmpegPath.Value); + Option maybeVersion = await GetVersion(ffmpegPath.Value, cancellationToken); if (maybeVersion.IsNone) { return WarningResult("Unable to determine ffmpeg version"); @@ -48,7 +49,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe foreach (ConfigElement ffprobePath in maybeFFprobePath) { - Option maybeVersion = await GetVersion(ffprobePath.Value); + Option maybeVersion = await GetVersion(ffprobePath.Value, cancellationToken); if (maybeVersion.IsNone) { return WarningResult("Unable to determine ffprobe version"); @@ -65,7 +66,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe return new HealthCheckResult("FFmpeg Version", HealthCheckStatus.Pass, string.Empty, None); } - + private Option ValidateVersion(string version, string app) { if (version.StartsWith("3.") || version.StartsWith("4.")) @@ -82,9 +83,9 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe return None; } - private static async Task> GetVersion(string path) + private static async Task> GetVersion(string path, CancellationToken cancellationToken) { - Option maybeLine = await GetProcessOutput(path, new[] { "-version" }) + Option maybeLine = await GetProcessOutput(path, new[] { "-version" }, cancellationToken) .Map(s => s.Split("\n").HeadOrNone().Map(h => h.Trim())); foreach (string line in maybeLine) { diff --git a/ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs index bf3297c16..834e07b03 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs @@ -16,58 +16,58 @@ public class FileNotFoundHealthCheck : BaseHealthCheck, IFileNotFoundHealthCheck protected override string Title => "File Not Found"; - public async Task Check() + public async Task Check(CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - List episodes = await dbContext.Episodes - .Filter(e => e.State == MediaItemState.FileNotFound) - .Include(e => e.MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); + List episodes = await dbContext.Episodes + .Filter(e => e.State == MediaItemState.FileNotFound) + .Include(e => e.MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .ToListAsync(cancellationToken); - List movies = await dbContext.Movies - .Filter(m => m.State == MediaItemState.FileNotFound) - .Include(m => m.MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); - - List musicVideos = await dbContext.MusicVideos - .Filter(mv => mv.State == MediaItemState.FileNotFound) - .Include(mv => mv.MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); - - List otherVideos = await dbContext.OtherVideos - .Filter(ov => ov.State == MediaItemState.FileNotFound) - .Include(ov => ov.MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); + List movies = await dbContext.Movies + .Filter(m => m.State == MediaItemState.FileNotFound) + .Include(m => m.MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .ToListAsync(cancellationToken); - List songs = await dbContext.Songs - .Filter(s => s.State == MediaItemState.FileNotFound) - .Include(s => s.MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); - - var all = movies.Map(m => m.MediaVersions.Head().MediaFiles.Head().Path) - .Append(episodes.Map(e => e.MediaVersions.Head().MediaFiles.Head().Path)) - .Append(musicVideos.Map(mv => mv.GetHeadVersion().MediaFiles.Head().Path)) - .Append(otherVideos.Map(ov => ov.GetHeadVersion().MediaFiles.Head().Path)) - .Append(songs.Map(s => s.GetHeadVersion().MediaFiles.Head().Path)) - .ToList(); + List musicVideos = await dbContext.MusicVideos + .Filter(mv => mv.State == MediaItemState.FileNotFound) + .Include(mv => mv.MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .ToListAsync(cancellationToken); - if (all.Any()) - { - var paths = all.Take(5).ToList(); + List otherVideos = await dbContext.OtherVideos + .Filter(ov => ov.State == MediaItemState.FileNotFound) + .Include(ov => ov.MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .ToListAsync(cancellationToken); - var files = string.Join(", ", paths); + List songs = await dbContext.Songs + .Filter(s => s.State == MediaItemState.FileNotFound) + .Include(s => s.MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .ToListAsync(cancellationToken); - return WarningResult( - $"There are {all.Count} files that do not exist on disk, including the following: {files}", - "/media/trash"); - } + var all = movies.Map(m => m.MediaVersions.Head().MediaFiles.Head().Path) + .Append(episodes.Map(e => e.MediaVersions.Head().MediaFiles.Head().Path)) + .Append(musicVideos.Map(mv => mv.GetHeadVersion().MediaFiles.Head().Path)) + .Append(otherVideos.Map(ov => ov.GetHeadVersion().MediaFiles.Head().Path)) + .Append(songs.Map(s => s.GetHeadVersion().MediaFiles.Head().Path)) + .ToList(); - return OkResult(); + if (all.Any()) + { + var paths = all.Take(5).ToList(); + + var files = string.Join(", ", paths); + + return WarningResult( + $"There are {all.Count} files that do not exist on disk, including the following: {files}", + "/media/trash"); + } + + return OkResult(); } } \ No newline at end of file diff --git a/ErsatzTV.Infrastructure/Health/Checks/HardwareAccelerationHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/HardwareAccelerationHealthCheck.cs index 11802ca19..de38bca2e 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/HardwareAccelerationHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/HardwareAccelerationHealthCheck.cs @@ -22,7 +22,7 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler _configElementRepository = configElementRepository; } - public async Task Check() + public async Task Check(CancellationToken cancellationToken) { Option maybeFFmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath); if (maybeFFmpegPath.IsNone) @@ -49,7 +49,8 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler if (!accelerationKinds.Any()) { - accelerationKinds.AddRange(await GetSupportedAccelerationKinds(maybeFFmpegPath.ValueUnsafe().Value)); + accelerationKinds.AddRange( + await GetSupportedAccelerationKinds(maybeFFmpegPath.ValueUnsafe().Value, cancellationToken)); } if (!accelerationKinds.Any()) @@ -69,7 +70,7 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler private async Task> VerifyProfilesUseAcceleration( IEnumerable accelerationKinds) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); List badChannels = await dbContext.Channels .Filter(c => c.StreamingMode != StreamingMode.HttpLiveStreamingDirect) @@ -87,11 +88,13 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler return None; } - private static async Task> GetSupportedAccelerationKinds(string ffmpegPath) + private static async Task> GetSupportedAccelerationKinds( + string ffmpegPath, + CancellationToken cancellationToken) { var result = new System.Collections.Generic.HashSet(); - - string output = await GetProcessOutput(ffmpegPath, new[] { "-v", "quiet", "-hwaccels" }); + + string output = await GetProcessOutput(ffmpegPath, new[] { "-v", "quiet", "-hwaccels" }, cancellationToken); foreach (string method in output.Split("\n").Map(s => s.Trim()).Skip(1)) { switch (method) diff --git a/ErsatzTV.Infrastructure/Health/Checks/MovieMetadataHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/MovieMetadataHealthCheck.cs index b99e21e3f..0a69bea0d 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/MovieMetadataHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/MovieMetadataHealthCheck.cs @@ -14,16 +14,16 @@ public class MovieMetadataHealthCheck : BaseHealthCheck, IMovieMetadataHealthChe _dbContextFactory = dbContextFactory; protected override string Title => "Movie Metadata"; - - public async Task Check() + + public async Task Check(CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + List movies = await dbContext.Movies .Filter(e => e.MovieMetadata.Count == 0) .Include(e => e.MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); + .ToListAsync(cancellationToken); if (movies.Any()) { @@ -36,7 +36,8 @@ public class MovieMetadataHealthCheck : BaseHealthCheck, IMovieMetadataHealthChe var folders = string.Join(", ", paths); - return WarningResult($"There are {movies.Count} movies with missing metadata, including in the following folders: {folders}"); + return WarningResult( + $"There are {movies.Count} movies with missing metadata, including in the following folders: {folders}"); } return OkResult(); diff --git a/ErsatzTV.Infrastructure/Health/Checks/VaapiDriverHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/VaapiDriverHealthCheck.cs index bbdcb501e..e041a0b5c 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/VaapiDriverHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/VaapiDriverHealthCheck.cs @@ -18,13 +18,13 @@ public class VaapiDriverHealthCheck : BaseHealthCheck, IVaapiDriverHealthCheck protected override string Title => "VAAPI Driver"; - public async Task Check() + public async Task Check(CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); List profiles = await dbContext.FFmpegProfiles .Filter(p => p.HardwareAcceleration == HardwareAccelerationKind.Vaapi) - .ToListAsync(); - + .ToListAsync(cancellationToken); + if (profiles.Count == 0) { return NotApplicableResult(); diff --git a/ErsatzTV.Infrastructure/Health/Checks/ZeroDurationHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/ZeroDurationHealthCheck.cs index f79a7a1af..21e460b09 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/ZeroDurationHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/ZeroDurationHealthCheck.cs @@ -16,40 +16,40 @@ public class ZeroDurationHealthCheck : BaseHealthCheck, IZeroDurationHealthCheck protected override string Title => "Zero Duration"; - public async Task Check() + public async Task Check(CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); List episodes = await dbContext.Episodes .Filter(e => e.MediaVersions.Any(mv => mv.Duration == TimeSpan.Zero)) .Include(e => e.MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); + .ToListAsync(cancellationToken); List movies = await dbContext.Movies .Filter(m => m.MediaVersions.Any(mv => mv.Duration == TimeSpan.Zero)) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); - + .ToListAsync(cancellationToken); + List musicVideos = await dbContext.MusicVideos .Filter(mv => mv.MediaVersions.Any(v => v.Duration == TimeSpan.Zero)) .Include(mv => mv.MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); - + .ToListAsync(cancellationToken); + List otherVideos = await dbContext.OtherVideos .Filter(ov => ov.MediaVersions.Any(mv => mv.Duration == TimeSpan.Zero)) .Include(ov => ov.MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); + .ToListAsync(cancellationToken); List songs = await dbContext.Songs .Filter(s => s.MediaVersions.Any(mv => mv.Duration == TimeSpan.Zero)) .Include(s => s.MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(); - + .ToListAsync(cancellationToken); + var all = movies.Map(m => m.MediaVersions.Head().MediaFiles.Head().Path) .Append(episodes.Map(e => e.MediaVersions.Head().MediaFiles.Head().Path)) .Append(musicVideos.Map(mv => mv.GetHeadVersion().MediaFiles.Head().Path)) diff --git a/ErsatzTV.Infrastructure/Health/HealthCheckService.cs b/ErsatzTV.Infrastructure/Health/HealthCheckService.cs index 955cab367..2c83b469f 100644 --- a/ErsatzTV.Infrastructure/Health/HealthCheckService.cs +++ b/ErsatzTV.Infrastructure/Health/HealthCheckService.cs @@ -33,6 +33,6 @@ public class HealthCheckService : IHealthCheckService }; } - public Task> PerformHealthChecks() => - _checks.Map(c => c.Check()).SequenceParallel().Map(results => results.ToList()); + public Task> PerformHealthChecks(CancellationToken cancellationToken) => + _checks.Map(c => c.Check(cancellationToken)).SequenceParallel().Map(results => results.ToList()); } \ No newline at end of file