Browse Source

use wrapped processes; fix hls pts bug (#690)

pull/692/head
Jason Dove 4 years ago committed by GitHub
parent
commit
f0670b345f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs
  2. 27
      ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs
  3. 9
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  4. 166
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  5. 5
      ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs
  6. 3
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs
  7. 34
      ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs
  8. 6
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  9. 3
      ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs
  10. 1
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  11. 69
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  12. 36
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  13. 6
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  14. 10
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  15. 6
      ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs
  16. 2
      ErsatzTV.Core/Health/IHealthCheck.cs
  17. 2
      ErsatzTV.Core/Health/IHealthCheckService.cs
  18. 3
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  19. 3
      ErsatzTV.Core/Interfaces/FFmpeg/ISongVideoGenerator.cs
  20. 3
      ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs
  21. 3
      ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs
  22. 3
      ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs
  23. 3
      ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs
  24. 18
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  25. 12
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  26. 25
      ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs
  27. 27
      ErsatzTV.Core/Metadata/SongFolderScanner.cs
  28. 43
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  29. 1
      ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj
  30. 1
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  31. 36
      ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs
  32. 13
      ErsatzTV.Infrastructure/Health/Checks/EpisodeMetadataHealthCheck.cs
  33. 5
      ErsatzTV.Infrastructure/Health/Checks/ErrorReportsHealthCheck.cs
  34. 2
      ErsatzTV.Infrastructure/Health/Checks/FFmpegReportsHealthCheck.cs
  35. 13
      ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs
  36. 90
      ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs
  37. 15
      ErsatzTV.Infrastructure/Health/Checks/HardwareAccelerationHealthCheck.cs
  38. 13
      ErsatzTV.Infrastructure/Health/Checks/MovieMetadataHealthCheck.cs
  39. 8
      ErsatzTV.Infrastructure/Health/Checks/VaapiDriverHealthCheck.cs
  40. 20
      ErsatzTV.Infrastructure/Health/Checks/ZeroDurationHealthCheck.cs
  41. 4
      ErsatzTV.Infrastructure/Health/HealthCheckService.cs

2
ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs

@ -13,7 +13,7 @@ public class GetAllHealthCheckResultsHandler : IRequestHandler<GetAllHealthCheck @@ -13,7 +13,7 @@ public class GetAllHealthCheckResultsHandler : IRequestHandler<GetAllHealthCheck
GetAllHealthCheckResults request,
CancellationToken cancellationToken)
{
List<HealthCheckResult> results = await _healthCheckService.PerformHealthChecks();
List<HealthCheckResult> results = await _healthCheckService.PerformHealthChecks(cancellationToken);
return results.Filter(r => r.Status != HealthCheckStatus.NotApplicable).ToList();
}
}

27
ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs

@ -47,21 +47,20 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei @@ -47,21 +47,20 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
Task<Either<BaseError, string>> IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>.Handle(
ForceScanLocalLibrary request,
CancellationToken cancellationToken) => Handle(request);
CancellationToken cancellationToken) => Handle(request, cancellationToken);
public Task<Either<BaseError, string>> Handle(
Task<Either<BaseError, string>> IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>.Handle(
ScanLocalLibraryIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
CancellationToken cancellationToken) => Handle(request, cancellationToken);
private Task<Either<BaseError, string>>
Handle(IScanLocalLibrary request) =>
private Task<Either<BaseError, string>> 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<Unit> PerformScan(RequestParameters parameters)
private async Task<Unit> PerformScan(RequestParameters parameters, CancellationToken cancellationToken)
{
(LocalLibrary localLibrary, string ffprobePath, string ffmpegPath, bool forceScan,
int libraryRefreshInterval) = parameters;
@ -92,7 +91,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei @@ -92,7 +91,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
ffmpegPath,
ffprobePath,
progressMin,
progressMax);
progressMax,
cancellationToken);
break;
case LibraryMediaKind.Shows:
await _televisionFolderScanner.ScanFolder(
@ -100,7 +100,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei @@ -100,7 +100,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
ffmpegPath,
ffprobePath,
progressMin,
progressMax);
progressMax,
cancellationToken);
break;
case LibraryMediaKind.MusicVideos:
await _musicVideoFolderScanner.ScanFolder(
@ -108,7 +109,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei @@ -108,7 +109,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
ffmpegPath,
ffprobePath,
progressMin,
progressMax);
progressMax,
cancellationToken);
break;
case LibraryMediaKind.OtherVideos:
await _otherVideoFolderScanner.ScanFolder(
@ -124,7 +126,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei @@ -124,7 +126,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
ffprobePath,
ffmpegPath,
progressMin,
progressMax);
progressMax,
cancellationToken);
break;
}

9
ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs

@ -10,13 +10,12 @@ using Microsoft.Extensions.Logging; @@ -10,13 +10,12 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Streaming;
public class StartFFmpegSessionHandler : MediatR.IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
{
private readonly ILogger<StartFFmpegSessionHandler> _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<StartFFmpegSess @@ -24,15 +23,13 @@ public class StartFFmpegSessionHandler : MediatR.IRequestHandler<StartFFmpegSess
ILogger<StartFFmpegSessionHandler> 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<Either<BaseError, Unit>> Handle(StartFFmpegSession request, CancellationToken cancellationToken) =>
@ -54,7 +51,7 @@ public class StartFFmpegSessionHandler : MediatR.IRequestHandler<StartFFmpegSess @@ -54,7 +51,7 @@ public class StartFFmpegSessionHandler : MediatR.IRequestHandler<StartFFmpegSess
_ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker);
// fire and forget worker
_ = worker.Run(request.ChannelNumber, idleTimeout)
_ = worker.Run(request.ChannelNumber, idleTimeout, cancellationToken)
.ContinueWith(
_ => _ffmpegSegmenterService.SessionWorkers.TryRemove(
request.ChannelNumber,

166
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -1,11 +1,15 @@ @@ -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 @@ -55,19 +59,27 @@ public class HlsSessionWorker : IHlsSessionWorker
DateTimeOffset filterBefore,
CancellationToken cancellationToken)
{
Option<string[]> maybeLines = await ReadPlaylistLines(cancellationToken);
return maybeLines.Map(input => _hlsPlaylistFilter.TrimPlaylist(PlaylistStart, filterBefore, input));
await Slim.WaitAsync(cancellationToken);
try
{
Option<string[]> 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 @@ -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<IMediator>();
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();
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 @@ -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<IClient>();
@ -239,49 +253,61 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -239,49 +253,61 @@ public class HlsSessionWorker : IHlsSessionWorker
private async Task TrimAndDelete(CancellationToken cancellationToken)
{
Option<string[]> 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<string[]> 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<long> GetPtsOffset(IMediator mediator, string channelNumber, CancellationToken cancellationToken)
private static async Task<long> GetPtsOffset(
IMediator mediator,
string channelNumber,
CancellationToken cancellationToken)
{
await Slim.WaitAsync(cancellationToken);
try
@ -315,41 +341,25 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -315,41 +341,25 @@ public class HlsSessionWorker : IHlsSessionWorker
private async Task<Option<string[]>> 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);
}

5
ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs

@ -19,7 +19,7 @@ public abstract class FFmpegProcessHandler<T> : IRequestHandler<T, Either<BaseEr @@ -19,7 +19,7 @@ public abstract class FFmpegProcessHandler<T> : IRequestHandler<T, Either<BaseEr
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Tuple<Channel, string>> 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<Either<BaseError, PlayoutItemProcessModel>>(error.Join()));
}
@ -27,7 +27,8 @@ public abstract class FFmpegProcessHandler<T> : IRequestHandler<T, Either<BaseEr @@ -27,7 +27,8 @@ public abstract class FFmpegProcessHandler<T> : IRequestHandler<T, Either<BaseEr
TvContext dbContext,
T request,
Channel channel,
string ffmpegPath);
string ffmpegPath,
CancellationToken cancellationToken);
private static async Task<Validation<BaseError, Tuple<Channel, string>>> Validate(
TvContext dbContext,

3
ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs

@ -25,7 +25,8 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetCo @@ -25,7 +25,8 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetCo
TvContext dbContext,
GetConcatProcessByChannelNumber request,
Channel channel,
string ffmpegPath)
string ffmpegPath,
CancellationToken cancellationToken)
{
bool saveReports = await dbContext.ConfigElements
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)

34
ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs

@ -57,42 +57,42 @@ public class GetLastPtsDurationHandler : IRequestHandler<GetLastPtsDuration, Eit @@ -57,42 +57,42 @@ public class GetLastPtsDurationHandler : IRequestHandler<GetLastPtsDuration, Eit
{
string[] argumentList =
{
// `-v 0` seems to prevent ffprobe from outputting anything on windows
"-v", "0",
"-show_entries",
"packet=pts,duration",
"-of",
"compact=p=0:nk=1",
"-read_intervals",
"-999999",
"-of", "compact=p=0:nk=1",
// "-read_intervals", "999999", // read_intervals causes inconsistent behavior on windows
segment.FullName
};
BufferedCommandResult probe = await Cli.Wrap(parameters.FFprobePath)
string lastLine = string.Empty;
Action<string> 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<string> 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);
}
}

6
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -58,7 +58,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -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< @@ -129,7 +130,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
song,
channel,
maybeGlobalWatermark,
ffmpegPath);
ffmpegPath,
cancellationToken);
}
bool saveReports = await dbContext.ConfigElements

3
ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs

@ -25,7 +25,8 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetW @@ -25,7 +25,8 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetW
TvContext dbContext,
GetWrappedProcessByChannelNumber request,
Channel channel,
string ffmpegPath)
string ffmpegPath,
CancellationToken cancellationToken)
{
bool saveReports = await dbContext.ConfigElements
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)

1
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -8,6 +8,7 @@ @@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.0.0" />
<PackageReference Include="CliWrap" Version="3.4.1" />
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="LanguageExt.Core" Version="4.0.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />

69
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs

@ -2,6 +2,7 @@ @@ -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 @@ -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 @@ -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 @@ -235,7 +240,7 @@ public class TranscodingTests
}
var imageCache = new Mock<IImageCache>();
// always return the static watermark resource
imageCache.Setup(
ic => ic.GetPathForImage(
@ -269,11 +274,12 @@ public class TranscodingTests @@ -269,11 +274,12 @@ public class TranscodingTests
var metadataRepository = new Mock<IMetadataRepository>();
metadataRepository
.Setup(r => r.UpdateLocalStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
.Callback<MediaItem, MediaVersion, bool>((_, version, _) =>
{
version.MediaFiles = v.MediaFiles;
v = version;
});
.Callback<MediaItem, MediaVersion, bool>(
(_, version, _) =>
{
version.MediaFiles = v.MediaFiles;
v = version;
});
var localStatisticsProvider = new LocalStatisticsProvider(
metadataRepository.Object,
@ -345,7 +351,7 @@ public class TranscodingTests @@ -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 @@ -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 @@ -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<string> 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 @@ -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))

36
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -98,7 +98,8 @@ public class MovieFolderScannerTests @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -590,7 +601,8 @@ public class MovieFolderScannerTests
FFmpegPath,
FFprobePath,
0,
1);
1,
CancellationToken.None);
result.IsRight.Should().BeTrue();

6
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -301,7 +301,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -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 @@ -314,7 +315,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
watermarkLocation,
horizontalMarginPercent,
verticalMarginPercent,
watermarkWidthPercent);
watermarkWidthPercent,
cancellationToken);
private Process GetProcess(
string ffmpegPath,

10
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -1,5 +1,6 @@ @@ -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 @@ -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 @@ -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;
}

6
ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs

@ -30,7 +30,8 @@ public class SongVideoGenerator : ISongVideoGenerator @@ -30,7 +30,8 @@ public class SongVideoGenerator : ISongVideoGenerator
Song song,
Channel channel,
Option<ChannelWatermark> maybeGlobalWatermark,
string ffmpegPath)
string ffmpegPath,
CancellationToken cancellationToken)
{
Option<string> subtitleFile = None;
@ -224,7 +225,8 @@ public class SongVideoGenerator : ISongVideoGenerator @@ -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())
{

2
ErsatzTV.Core/Health/IHealthCheck.cs

@ -2,5 +2,5 @@ @@ -2,5 +2,5 @@
public interface IHealthCheck
{
Task<HealthCheckResult> Check();
Task<HealthCheckResult> Check(CancellationToken cancellationToken);
}

2
ErsatzTV.Core/Health/IHealthCheckService.cs

@ -2,5 +2,5 @@ @@ -2,5 +2,5 @@
public interface IHealthCheckService
{
Task<List<HealthCheckResult>> PerformHealthChecks();
Task<List<HealthCheckResult>> PerformHealthChecks(CancellationToken cancellationToken);
}

3
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs

@ -57,5 +57,6 @@ public interface IFFmpegProcessService @@ -57,5 +57,6 @@ public interface IFFmpegProcessService
WatermarkLocation watermarkLocation,
int horizontalMarginPercent,
int verticalMarginPercent,
int watermarkWidthPercent);
int watermarkWidthPercent,
CancellationToken cancellationToken);
}

3
ErsatzTV.Core/Interfaces/FFmpeg/ISongVideoGenerator.cs

@ -8,5 +8,6 @@ public interface ISongVideoGenerator @@ -8,5 +8,6 @@ public interface ISongVideoGenerator
Song song,
Channel channel,
Option<ChannelWatermark> maybeGlobalWatermark,
string ffmpegPath);
string ffmpegPath,
CancellationToken cancellationToken);
}

3
ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs

@ -9,5 +9,6 @@ public interface IMovieFolderScanner @@ -9,5 +9,6 @@ public interface IMovieFolderScanner
string ffmpegPath,
string ffprobePath,
decimal progressMin,
decimal progressMax);
decimal progressMax,
CancellationToken cancellationToken);
}

3
ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs

@ -9,5 +9,6 @@ public interface IMusicVideoFolderScanner @@ -9,5 +9,6 @@ public interface IMusicVideoFolderScanner
string ffmpegPath,
string ffprobePath,
decimal progressMin,
decimal progressMax);
decimal progressMax,
CancellationToken cancellationToken);
}

3
ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs

@ -9,5 +9,6 @@ public interface ISongFolderScanner @@ -9,5 +9,6 @@ public interface ISongFolderScanner
string ffprobePath,
string ffmpegPath,
decimal progressMin,
decimal progressMax);
decimal progressMax,
CancellationToken cancellationToken);
}

3
ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs

@ -9,5 +9,6 @@ public interface ITelevisionFolderScanner @@ -9,5 +9,6 @@ public interface ITelevisionFolderScanner
string ffmpegPath,
string ffprobePath,
decimal progressMin,
decimal progressMax);
decimal progressMax,
CancellationToken cancellationToken);
}

18
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -1,5 +1,6 @@ @@ -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 @@ -124,7 +125,8 @@ public abstract class LocalFolderScanner
Domain.Metadata metadata,
ArtworkKind artworkKind,
Option<string> ffmpegPath,
Option<int> attachedPicIndex)
Option<int> attachedPicIndex,
CancellationToken cancellationToken)
{
DateTime lastWriteTime = _localFileSystem.GetLastWriteTime(artworkFile);
@ -168,8 +170,11 @@ public abstract class LocalFolderScanner @@ -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 @@ -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;
});

12
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -67,7 +67,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -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 @@ -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 @@ -229,7 +230,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
private async Task<Either<BaseError, MediaItemScanResult<Movie>>> UpdateArtwork(
MediaItemScanResult<Movie> result,
ArtworkKind artworkKind)
ArtworkKind artworkKind,
CancellationToken cancellationToken)
{
try
{
@ -238,7 +240,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -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;

25
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

@ -68,7 +68,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -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 @@ -88,8 +89,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
Either<BaseError, MediaItemScanResult<Artist>> 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 @@ -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 @@ -212,7 +214,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
private async Task<Either<BaseError, MediaItemScanResult<Artist>>> UpdateArtworkForArtist(
MediaItemScanResult<Artist> result,
string artistFolder,
ArtworkKind artworkKind)
ArtworkKind artworkKind,
CancellationToken cancellationToken)
{
try
{
@ -221,7 +224,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -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 @@ -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<string>();
folderQueue.Enqueue(artistFolder);
@ -277,7 +281,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -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 @@ -379,7 +383,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
}
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateThumbnail(
MediaItemScanResult<MusicVideo> result)
MediaItemScanResult<MusicVideo> result,
CancellationToken cancellationToken)
{
try
{
@ -388,7 +393,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -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;

27
ErsatzTV.Core/Metadata/SongFolderScanner.cs

@ -66,7 +66,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -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 @@ -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 @@ -212,7 +213,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
private async Task<Either<BaseError, MediaItemScanResult<Song>>> UpdateThumbnail(
MediaItemScanResult<Song> result,
string ffmpegPath)
string ffmpegPath,
CancellationToken cancellationToken)
{
try
{
@ -234,10 +236,16 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -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 @@ -246,7 +254,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
return BaseError.New(ex.ToString());
}
}
private Option<string> LocateThumbnail(Song song)
{
string path = song.MediaVersions.Head().MediaFiles.Head().Path;
@ -263,7 +271,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -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<MediaStream> maybeArtworkStream = Optional(song.GetHeadVersion().Streams.Find(ms => ms.AttachedPic));
foreach (MediaStream artworkStream in maybeArtworkStream)
@ -273,7 +281,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -273,7 +281,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
song.SongMetadata.Head(),
ArtworkKind.Thumbnail,
ffmpegPath,
artworkStream.Index);
artworkStream.Index,
cancellationToken);
}
}
}

43
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -67,7 +67,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -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 @@ -85,9 +86,9 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
Either<BaseError, MediaItemScanResult<Show>> 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 @@ -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 @@ -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 @@ -182,12 +185,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
Either<BaseError, Season> 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 @@ -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 @@ -232,7 +236,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
episode => UpdateStatistics(new MediaItemScanResult<Episode>(episode), ffmpegPath, ffprobePath)
.MapT(_ => episode))
.BindT(UpdateMetadata)
.BindT(UpdateThumbnail)
.BindT(e => UpdateThumbnail(e, cancellationToken))
.BindT(e => FlagNormal(new MediaItemScanResult<Episode>(e)))
.MapT(r => r.Item);
@ -363,7 +367,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -363,7 +367,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
private async Task<Either<BaseError, MediaItemScanResult<Show>>> UpdateArtworkForShow(
MediaItemScanResult<Show> result,
string showFolder,
ArtworkKind artworkKind)
ArtworkKind artworkKind,
CancellationToken cancellationToken)
{
try
{
@ -372,7 +377,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -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 @@ -384,7 +389,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
}
}
private async Task<Either<BaseError, Season>> UpdatePoster(Season season, string seasonFolder)
private async Task<Either<BaseError, Season>> UpdatePoster(Season season, string seasonFolder, CancellationToken cancellationToken)
{
try
{
@ -392,7 +397,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -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 @@ -404,7 +409,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
}
}
private async Task<Either<BaseError, Episode>> UpdateThumbnail(Episode episode)
private async Task<Either<BaseError, Episode>> UpdateThumbnail(Episode episode, CancellationToken cancellationToken)
{
try
{
@ -413,7 +418,13 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -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);
}
});

1
ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj

@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.4.1" />
<PackageReference Include="LanguageExt.Core" Version="4.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
</ItemGroup>

1
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Blurhash.ImageSharp" Version="1.1.1" />
<PackageReference Include="CliWrap" Version="3.4.1" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" />

36
ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs

@ -1,6 +1,6 @@ @@ -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 @@ -28,27 +28,17 @@ public abstract class BaseHealthCheck
protected HealthCheckResult InfoResult(string message) =>
new(Title, HealthCheckStatus.Info, message, None);
protected static async Task<string> GetProcessOutput(string path, IEnumerable<string> arguments)
protected static async Task<string> GetProcessOutput(
string path,
IEnumerable<string> 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;
}
}

13
ErsatzTV.Infrastructure/Health/Checks/EpisodeMetadataHealthCheck.cs

@ -14,16 +14,16 @@ public class EpisodeMetadataHealthCheck : BaseHealthCheck, IEpisodeMetadataHealt @@ -14,16 +14,16 @@ public class EpisodeMetadataHealthCheck : BaseHealthCheck, IEpisodeMetadataHealt
_dbContextFactory = dbContextFactory;
protected override string Title => "Episode Metadata";
public async Task<HealthCheckResult> Check()
public async Task<HealthCheckResult> Check(CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Episode> 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 @@ -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();

5
ErsatzTV.Infrastructure/Health/Checks/ErrorReportsHealthCheck.cs

@ -16,7 +16,7 @@ public class ErrorReportsHealthCheck : BaseHealthCheck, IErrorReportsHealthCheck @@ -16,7 +16,7 @@ public class ErrorReportsHealthCheck : BaseHealthCheck, IErrorReportsHealthCheck
protected override string Title => "Error Reports";
public Task<HealthCheckResult> Check()
public Task<HealthCheckResult> Check(CancellationToken cancellationToken)
{
if (_bugsnagConfiguration.Value.Enable)
{
@ -26,6 +26,7 @@ public class ErrorReportsHealthCheck : BaseHealthCheck, IErrorReportsHealthCheck @@ -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();
}
}

2
ErsatzTV.Infrastructure/Health/Checks/FFmpegReportsHealthCheck.cs

@ -12,7 +12,7 @@ public class FFmpegReportsHealthCheck : BaseHealthCheck, IFFmpegReportsHealthChe @@ -12,7 +12,7 @@ public class FFmpegReportsHealthCheck : BaseHealthCheck, IFFmpegReportsHealthChe
public FFmpegReportsHealthCheck(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<HealthCheckResult> Check()
public async Task<HealthCheckResult> Check(CancellationToken cancellationToken)
{
Option<bool> saveReports =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);

13
ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs

@ -16,7 +16,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -16,7 +16,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
_configElementRepository = configElementRepository;
}
public async Task<HealthCheckResult> Check()
public async Task<HealthCheckResult> Check(CancellationToken cancellationToken)
{
Option<ConfigElement> maybeFFmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath);
if (maybeFFmpegPath.IsNone)
@ -29,9 +29,10 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -29,9 +29,10 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
{
return FailResult("Unable to locate ffprobe");
}
foreach (ConfigElement ffmpegPath in maybeFFmpegPath)
{
Option<string> maybeVersion = await GetVersion(ffmpegPath.Value);
Option<string> 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 @@ -48,7 +49,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
foreach (ConfigElement ffprobePath in maybeFFprobePath)
{
Option<string> maybeVersion = await GetVersion(ffprobePath.Value);
Option<string> 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 @@ -65,7 +66,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
return new HealthCheckResult("FFmpeg Version", HealthCheckStatus.Pass, string.Empty, None);
}
private Option<HealthCheckResult> ValidateVersion(string version, string app)
{
if (version.StartsWith("3.") || version.StartsWith("4."))
@ -82,9 +83,9 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -82,9 +83,9 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
return None;
}
private static async Task<Option<string>> GetVersion(string path)
private static async Task<Option<string>> GetVersion(string path, CancellationToken cancellationToken)
{
Option<string> maybeLine = await GetProcessOutput(path, new[] { "-version" })
Option<string> maybeLine = await GetProcessOutput(path, new[] { "-version" }, cancellationToken)
.Map(s => s.Split("\n").HeadOrNone().Map(h => h.Trim()));
foreach (string line in maybeLine)
{

90
ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs

@ -16,58 +16,58 @@ public class FileNotFoundHealthCheck : BaseHealthCheck, IFileNotFoundHealthCheck @@ -16,58 +16,58 @@ public class FileNotFoundHealthCheck : BaseHealthCheck, IFileNotFoundHealthCheck
protected override string Title => "File Not Found";
public async Task<HealthCheckResult> Check()
public async Task<HealthCheckResult> Check(CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Episode> episodes = await dbContext.Episodes
.Filter(e => e.State == MediaItemState.FileNotFound)
.Include(e => e.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.ToListAsync();
List<Episode> episodes = await dbContext.Episodes
.Filter(e => e.State == MediaItemState.FileNotFound)
.Include(e => e.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.ToListAsync(cancellationToken);
List<Movie> movies = await dbContext.Movies
.Filter(m => m.State == MediaItemState.FileNotFound)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.ToListAsync();
List<MusicVideo> musicVideos = await dbContext.MusicVideos
.Filter(mv => mv.State == MediaItemState.FileNotFound)
.Include(mv => mv.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.ToListAsync();
List<OtherVideo> otherVideos = await dbContext.OtherVideos
.Filter(ov => ov.State == MediaItemState.FileNotFound)
.Include(ov => ov.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.ToListAsync();
List<Movie> movies = await dbContext.Movies
.Filter(m => m.State == MediaItemState.FileNotFound)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.ToListAsync(cancellationToken);
List<Song> 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<MusicVideo> 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<OtherVideo> 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<Song> 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();
}
}

15
ErsatzTV.Infrastructure/Health/Checks/HardwareAccelerationHealthCheck.cs

@ -22,7 +22,7 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler @@ -22,7 +22,7 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler
_configElementRepository = configElementRepository;
}
public async Task<HealthCheckResult> Check()
public async Task<HealthCheckResult> Check(CancellationToken cancellationToken)
{
Option<ConfigElement> maybeFFmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath);
if (maybeFFmpegPath.IsNone)
@ -49,7 +49,8 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler @@ -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 @@ -69,7 +70,7 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler
private async Task<Option<HealthCheckResult>> VerifyProfilesUseAcceleration(
IEnumerable<HardwareAccelerationKind> accelerationKinds)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
List<Channel> badChannels = await dbContext.Channels
.Filter(c => c.StreamingMode != StreamingMode.HttpLiveStreamingDirect)
@ -87,11 +88,13 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler @@ -87,11 +88,13 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler
return None;
}
private static async Task<List<HardwareAccelerationKind>> GetSupportedAccelerationKinds(string ffmpegPath)
private static async Task<List<HardwareAccelerationKind>> GetSupportedAccelerationKinds(
string ffmpegPath,
CancellationToken cancellationToken)
{
var result = new System.Collections.Generic.HashSet<HardwareAccelerationKind>();
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)

13
ErsatzTV.Infrastructure/Health/Checks/MovieMetadataHealthCheck.cs

@ -14,16 +14,16 @@ public class MovieMetadataHealthCheck : BaseHealthCheck, IMovieMetadataHealthChe @@ -14,16 +14,16 @@ public class MovieMetadataHealthCheck : BaseHealthCheck, IMovieMetadataHealthChe
_dbContextFactory = dbContextFactory;
protected override string Title => "Movie Metadata";
public async Task<HealthCheckResult> Check()
public async Task<HealthCheckResult> Check(CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Movie> 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 @@ -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();

8
ErsatzTV.Infrastructure/Health/Checks/VaapiDriverHealthCheck.cs

@ -18,13 +18,13 @@ public class VaapiDriverHealthCheck : BaseHealthCheck, IVaapiDriverHealthCheck @@ -18,13 +18,13 @@ public class VaapiDriverHealthCheck : BaseHealthCheck, IVaapiDriverHealthCheck
protected override string Title => "VAAPI Driver";
public async Task<HealthCheckResult> Check()
public async Task<HealthCheckResult> Check(CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<FFmpegProfile> profiles = await dbContext.FFmpegProfiles
.Filter(p => p.HardwareAcceleration == HardwareAccelerationKind.Vaapi)
.ToListAsync();
.ToListAsync(cancellationToken);
if (profiles.Count == 0)
{
return NotApplicableResult();

20
ErsatzTV.Infrastructure/Health/Checks/ZeroDurationHealthCheck.cs

@ -16,40 +16,40 @@ public class ZeroDurationHealthCheck : BaseHealthCheck, IZeroDurationHealthCheck @@ -16,40 +16,40 @@ public class ZeroDurationHealthCheck : BaseHealthCheck, IZeroDurationHealthCheck
protected override string Title => "Zero Duration";
public async Task<HealthCheckResult> Check()
public async Task<HealthCheckResult> Check(CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Episode> 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<Movie> 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<MusicVideo> 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<OtherVideo> 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<Song> 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))

4
ErsatzTV.Infrastructure/Health/HealthCheckService.cs

@ -33,6 +33,6 @@ public class HealthCheckService : IHealthCheckService @@ -33,6 +33,6 @@ public class HealthCheckService : IHealthCheckService
};
}
public Task<List<HealthCheckResult>> PerformHealthChecks() =>
_checks.Map(c => c.Check()).SequenceParallel().Map(results => results.ToList());
public Task<List<HealthCheckResult>> PerformHealthChecks(CancellationToken cancellationToken) =>
_checks.Map(c => c.Check(cancellationToken)).SequenceParallel().Map(results => results.ToList());
}
Loading…
Cancel
Save