mirror of https://github.com/ErsatzTV/ErsatzTV.git
23 changed files with 695 additions and 1 deletions
@ -0,0 +1,8 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Health.Queries |
||||||
|
{ |
||||||
|
public record GetAllHealthCheckResults : IRequest<List<HealthCheckResult>>; |
||||||
|
} |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Health.Queries |
||||||
|
{ |
||||||
|
public class GetAllHealthCheckResultsHandler : IRequestHandler<GetAllHealthCheckResults, List<HealthCheckResult>> |
||||||
|
{ |
||||||
|
private readonly IHealthCheckService _healthCheckService; |
||||||
|
|
||||||
|
public GetAllHealthCheckResultsHandler(IHealthCheckService healthCheckService) => |
||||||
|
_healthCheckService = healthCheckService; |
||||||
|
|
||||||
|
public Task<List<HealthCheckResult>> Handle( |
||||||
|
GetAllHealthCheckResults request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
_healthCheckService.PerformHealthChecks(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace ErsatzTV.Core.Health.Checks |
||||||
|
{ |
||||||
|
public interface IEpisodeMetadataHealthCheck : IHealthCheck |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace ErsatzTV.Core.Health.Checks |
||||||
|
{ |
||||||
|
public interface IFFmpegReportsHealthCheck : IHealthCheck |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace ErsatzTV.Core.Health.Checks |
||||||
|
{ |
||||||
|
public interface IFFmpegVersionHealthCheck : IHealthCheck |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace ErsatzTV.Core.Health.Checks |
||||||
|
{ |
||||||
|
public interface IHardwareAccelerationHealthCheck : IHealthCheck |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace ErsatzTV.Core.Health.Checks |
||||||
|
{ |
||||||
|
public interface IMovieMetadataHealthCheck : IHealthCheck |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace ErsatzTV.Core.Health.Checks |
||||||
|
{ |
||||||
|
public interface IZeroDurationHealthCheck : IHealthCheck |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
namespace ErsatzTV.Core.Health |
||||||
|
{ |
||||||
|
public record HealthCheckResult(string Title, HealthCheckStatus Status, string Message); |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
namespace ErsatzTV.Core.Health |
||||||
|
{ |
||||||
|
public enum HealthCheckStatus |
||||||
|
{ |
||||||
|
Pass, |
||||||
|
Fail, |
||||||
|
Warning, |
||||||
|
Info |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Health |
||||||
|
{ |
||||||
|
public interface IHealthCheck |
||||||
|
{ |
||||||
|
Task<HealthCheckResult> Check(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Health |
||||||
|
{ |
||||||
|
public interface IHealthCheckService |
||||||
|
{ |
||||||
|
Task<List<HealthCheckResult>> PerformHealthChecks(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Diagnostics; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using LanguageExt; |
||||||
|
using Lucene.Net.Util; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Health.Checks |
||||||
|
{ |
||||||
|
public abstract class BaseHealthCheck |
||||||
|
{ |
||||||
|
protected abstract string Title { get; } |
||||||
|
|
||||||
|
protected HealthCheckResult Result(HealthCheckStatus status, string message) => |
||||||
|
new(Title, status, message); |
||||||
|
|
||||||
|
protected HealthCheckResult OkResult() => |
||||||
|
new(Title, HealthCheckStatus.Pass, string.Empty); |
||||||
|
|
||||||
|
protected HealthCheckResult FailResult(string message) => |
||||||
|
new(Title, HealthCheckStatus.Fail, message); |
||||||
|
|
||||||
|
protected HealthCheckResult WarningResult(string message) => |
||||||
|
new(Title, HealthCheckStatus.Warning, message); |
||||||
|
|
||||||
|
protected HealthCheckResult InfoResult(string message) => |
||||||
|
new(Title, HealthCheckStatus.Info, message); |
||||||
|
|
||||||
|
protected static async Task<string> GetProcessOutput(string path, IEnumerable<string> arguments) |
||||||
|
{ |
||||||
|
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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using ErsatzTV.Core.Health.Checks; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using LanguageExt; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Health.Checks |
||||||
|
{ |
||||||
|
public class EpisodeMetadataHealthCheck : BaseHealthCheck, IEpisodeMetadataHealthCheck |
||||||
|
{ |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
|
||||||
|
public EpisodeMetadataHealthCheck(IDbContextFactory<TvContext> dbContextFactory) => |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
|
||||||
|
protected override string Title => "Episode Metadata"; |
||||||
|
|
||||||
|
public async Task<HealthCheckResult> Check() |
||||||
|
{ |
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
|
||||||
|
List<Episode> episodes = await dbContext.Episodes |
||||||
|
.Filter(e => e.EpisodeMetadata.Count == 0) |
||||||
|
.Include(e => e.MediaVersions) |
||||||
|
.ThenInclude(mv => mv.MediaFiles) |
||||||
|
.ToListAsync(); |
||||||
|
|
||||||
|
if (episodes.Any()) |
||||||
|
{ |
||||||
|
var paths = episodes.SelectMany(e => e.MediaVersions.Map(mv => mv.MediaFiles)) |
||||||
|
.Flatten() |
||||||
|
.Map(f => Optional<string>(Path.GetDirectoryName(f.Path))) |
||||||
|
.Sequence() |
||||||
|
.Flatten() |
||||||
|
.Distinct() |
||||||
|
.Take(5) |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
var folders = string.Join(", ", paths); |
||||||
|
|
||||||
|
return WarningResult($"There are {episodes.Count} episodes with missing metadata, including in the following folders: {folders}"); |
||||||
|
} |
||||||
|
|
||||||
|
return OkResult(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using ErsatzTV.Core.Health.Checks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Health.Checks |
||||||
|
{ |
||||||
|
public class FFmpegReportsHealthCheck : BaseHealthCheck, IFFmpegReportsHealthCheck |
||||||
|
{ |
||||||
|
private readonly IConfigElementRepository _configElementRepository; |
||||||
|
|
||||||
|
public FFmpegReportsHealthCheck(IConfigElementRepository configElementRepository) => |
||||||
|
_configElementRepository = configElementRepository; |
||||||
|
|
||||||
|
public async Task<HealthCheckResult> Check() |
||||||
|
{ |
||||||
|
Option<bool> saveReports = |
||||||
|
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports); |
||||||
|
|
||||||
|
foreach (bool value in saveReports) |
||||||
|
{ |
||||||
|
if (value) |
||||||
|
{ |
||||||
|
return Result( |
||||||
|
HealthCheckStatus.Warning, |
||||||
|
"FFmpeg troubleshooting reports are enabled and may use a lot of disk space"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return OkResult(); |
||||||
|
} |
||||||
|
|
||||||
|
protected override string Title => "FFmpeg Reports"; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,110 @@ |
|||||||
|
using System.Text.RegularExpressions; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using ErsatzTV.Core.Health.Checks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Health.Checks |
||||||
|
{ |
||||||
|
public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthCheck |
||||||
|
{ |
||||||
|
private readonly IConfigElementRepository _configElementRepository; |
||||||
|
|
||||||
|
public FFmpegVersionHealthCheck(IConfigElementRepository configElementRepository) |
||||||
|
{ |
||||||
|
_configElementRepository = configElementRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<HealthCheckResult> Check() |
||||||
|
{ |
||||||
|
Option<ConfigElement> maybeFFmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath); |
||||||
|
if (maybeFFmpegPath.IsNone) |
||||||
|
{ |
||||||
|
return FailResult("Unable to locate ffmpeg"); |
||||||
|
} |
||||||
|
|
||||||
|
Option<ConfigElement> maybeFFprobePath = await _configElementRepository.Get(ConfigElementKey.FFprobePath); |
||||||
|
if (maybeFFprobePath.IsNone) |
||||||
|
{ |
||||||
|
return FailResult("Unable to locate ffprobe"); |
||||||
|
} |
||||||
|
foreach (ConfigElement ffmpegPath in maybeFFmpegPath) |
||||||
|
{ |
||||||
|
Option<string> maybeVersion = await GetVersion(ffmpegPath.Value); |
||||||
|
if (maybeVersion.IsNone) |
||||||
|
{ |
||||||
|
return WarningResult("Unable to determine ffmpeg version"); |
||||||
|
} |
||||||
|
|
||||||
|
foreach (string version in maybeVersion) |
||||||
|
{ |
||||||
|
foreach (HealthCheckResult result in ValidateVersion(version, "ffmpeg")) |
||||||
|
{ |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
foreach (ConfigElement ffprobePath in maybeFFprobePath) |
||||||
|
{ |
||||||
|
Option<string> maybeVersion = await GetVersion(ffprobePath.Value); |
||||||
|
if (maybeVersion.IsNone) |
||||||
|
{ |
||||||
|
return WarningResult("Unable to determine ffprobe version"); |
||||||
|
} |
||||||
|
|
||||||
|
foreach (string version in maybeVersion) |
||||||
|
{ |
||||||
|
foreach (HealthCheckResult result in ValidateVersion(version, "ffprobe")) |
||||||
|
{ |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return new HealthCheckResult("FFmpeg Version", HealthCheckStatus.Pass, string.Empty); |
||||||
|
} |
||||||
|
|
||||||
|
private Option<HealthCheckResult> ValidateVersion(string version, string app) |
||||||
|
{ |
||||||
|
if (version.StartsWith("3.")) |
||||||
|
{ |
||||||
|
return FailResult($"{app} version {version} is too old; please install 4.3!"); |
||||||
|
} |
||||||
|
|
||||||
|
if (version.StartsWith("4.4")) |
||||||
|
{ |
||||||
|
return FailResult($"{app} version 4.4 is known to have issues; please install 4.3!"); |
||||||
|
} |
||||||
|
|
||||||
|
if (!version.StartsWith("4.3")) |
||||||
|
{ |
||||||
|
return WarningResult($"{app} version {version} is unexpected and may have problems; please install 4.3!"); |
||||||
|
} |
||||||
|
|
||||||
|
return None; |
||||||
|
} |
||||||
|
|
||||||
|
private static async Task<Option<string>> GetVersion(string path) |
||||||
|
{ |
||||||
|
Option<string> maybeLine = await GetProcessOutput(path, new[] { "-version" }) |
||||||
|
.Map(s => s.Split("\n").HeadOrNone().Map(h => h.Trim())); |
||||||
|
foreach (string line in maybeLine) |
||||||
|
{ |
||||||
|
const string PATTERN = @"version\s+([^\s]+)"; |
||||||
|
Match match = Regex.Match(line, PATTERN); |
||||||
|
if (match.Success) |
||||||
|
{ |
||||||
|
return match.Groups[1].Value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return None; |
||||||
|
} |
||||||
|
|
||||||
|
protected override string Title => "FFmpeg Version"; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,127 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Reflection; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using ErsatzTV.Core.Health.Checks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using LanguageExt; |
||||||
|
using LanguageExt.UnsafeValueAccess; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Health.Checks |
||||||
|
{ |
||||||
|
public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAccelerationHealthCheck |
||||||
|
{ |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
private readonly IConfigElementRepository _configElementRepository; |
||||||
|
|
||||||
|
public HardwareAccelerationHealthCheck( |
||||||
|
IDbContextFactory<TvContext> dbContextFactory, |
||||||
|
IConfigElementRepository configElementRepository) |
||||||
|
{ |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
_configElementRepository = configElementRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<HealthCheckResult> Check() |
||||||
|
{ |
||||||
|
Option<ConfigElement> maybeFFmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath); |
||||||
|
if (maybeFFmpegPath.IsNone) |
||||||
|
{ |
||||||
|
return FailResult("Unable to locate ffmpeg"); |
||||||
|
} |
||||||
|
|
||||||
|
string version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>() |
||||||
|
?.InformationalVersion ?? "unknown"; |
||||||
|
|
||||||
|
var accelerationKinds = new List<HardwareAccelerationKind>(); |
||||||
|
|
||||||
|
if (version.Contains("docker", StringComparison.OrdinalIgnoreCase)) |
||||||
|
{ |
||||||
|
if (version.Contains("nvidia", StringComparison.OrdinalIgnoreCase)) |
||||||
|
{ |
||||||
|
accelerationKinds.Add(HardwareAccelerationKind.Nvenc); |
||||||
|
} |
||||||
|
else if (version.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) |
||||||
|
{ |
||||||
|
accelerationKinds.Add(HardwareAccelerationKind.Vaapi); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!accelerationKinds.Any()) |
||||||
|
{ |
||||||
|
accelerationKinds.AddRange(await GetSupportedAccelerationKinds(maybeFFmpegPath.ValueUnsafe().Value)); |
||||||
|
} |
||||||
|
|
||||||
|
if (!accelerationKinds.Any()) |
||||||
|
{ |
||||||
|
return InfoResult("No compatible hardware acceleration kinds are supported by ffmpeg"); |
||||||
|
} |
||||||
|
|
||||||
|
Option<HealthCheckResult> maybeResult = await VerifyProfilesUseAcceleration(accelerationKinds); |
||||||
|
foreach (HealthCheckResult result in maybeResult) |
||||||
|
{ |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
return OkResult(); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Option<HealthCheckResult>> VerifyProfilesUseAcceleration( |
||||||
|
IEnumerable<HardwareAccelerationKind> accelerationKinds) |
||||||
|
{ |
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
|
||||||
|
List<Channel> badChannels = await dbContext.Channels |
||||||
|
.Filter(c => c.StreamingMode != StreamingMode.HttpLiveStreamingDirect) |
||||||
|
.Filter(c => !accelerationKinds.Contains(c.FFmpegProfile.HardwareAcceleration)) |
||||||
|
.ToListAsync(); |
||||||
|
|
||||||
|
if (badChannels.Any()) |
||||||
|
{ |
||||||
|
var accel = string.Join(", ", accelerationKinds); |
||||||
|
var channels = string.Join(", ", badChannels.Map(c => $"{c.Number} - {c.Name}")); |
||||||
|
return WarningResult( |
||||||
|
$"The following channels are transcoding without hardware acceleration ({accel}): {channels}"); |
||||||
|
} |
||||||
|
|
||||||
|
return None; |
||||||
|
} |
||||||
|
|
||||||
|
private static async Task<List<HardwareAccelerationKind>> GetSupportedAccelerationKinds(string ffmpegPath) |
||||||
|
{ |
||||||
|
var result = new System.Collections.Generic.HashSet<HardwareAccelerationKind>(); |
||||||
|
|
||||||
|
string output = await GetProcessOutput(ffmpegPath, new[] { "-v", "quiet", "-hwaccels" }); |
||||||
|
foreach (string method in output.Split("\n").Map(s => s.Trim()).Skip(1)) |
||||||
|
{ |
||||||
|
switch (method) |
||||||
|
{ |
||||||
|
case "vaapi": |
||||||
|
result.Add(HardwareAccelerationKind.Vaapi); |
||||||
|
break; |
||||||
|
case "nvenc": |
||||||
|
result.Add(HardwareAccelerationKind.Nvenc); |
||||||
|
break; |
||||||
|
case "qsv": |
||||||
|
// qsv is only supported on windows
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||||
|
{ |
||||||
|
result.Add(HardwareAccelerationKind.Qsv); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result.ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
protected override string Title => "Hardware Acceleration"; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using ErsatzTV.Core.Health.Checks; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using LanguageExt; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Health.Checks |
||||||
|
{ |
||||||
|
public class MovieMetadataHealthCheck : BaseHealthCheck, IMovieMetadataHealthCheck |
||||||
|
{ |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
|
||||||
|
public MovieMetadataHealthCheck(IDbContextFactory<TvContext> dbContextFactory) => |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
|
||||||
|
protected override string Title => "Movie Metadata"; |
||||||
|
|
||||||
|
public async Task<HealthCheckResult> Check() |
||||||
|
{ |
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
|
||||||
|
List<Movie> movies = await dbContext.Movies |
||||||
|
.Filter(e => e.MovieMetadata.Count == 0) |
||||||
|
.Include(e => e.MediaVersions) |
||||||
|
.ThenInclude(mv => mv.MediaFiles) |
||||||
|
.ToListAsync(); |
||||||
|
|
||||||
|
if (movies.Any()) |
||||||
|
{ |
||||||
|
var paths = movies.SelectMany(e => e.MediaVersions.Map(mv => mv.MediaFiles)) |
||||||
|
.Flatten() |
||||||
|
.Map(f => Optional<string>(Path.GetDirectoryName(f.Path))) |
||||||
|
.Sequence() |
||||||
|
.Flatten() |
||||||
|
.Distinct() |
||||||
|
.Take(5) |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
var folders = string.Join(", ", paths); |
||||||
|
|
||||||
|
return WarningResult($"There are {movies.Count} movies with missing metadata, including in the following folders: {folders}"); |
||||||
|
} |
||||||
|
|
||||||
|
return OkResult(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using ErsatzTV.Core.Health.Checks; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Health.Checks |
||||||
|
{ |
||||||
|
public class ZeroDurationHealthCheck : BaseHealthCheck, IZeroDurationHealthCheck |
||||||
|
{ |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
|
||||||
|
public ZeroDurationHealthCheck(IDbContextFactory<TvContext> dbContextFactory) => |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
|
||||||
|
protected override string Title => "Zero Duration"; |
||||||
|
|
||||||
|
public async Task<HealthCheckResult> Check() |
||||||
|
{ |
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
|
||||||
|
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(); |
||||||
|
|
||||||
|
List<Movie> movies = await dbContext.Movies |
||||||
|
.Filter(e => e.MediaVersions.Any(mv => mv.Duration == TimeSpan.Zero)) |
||||||
|
.Include(e => e.MediaVersions) |
||||||
|
.ThenInclude(mv => mv.MediaFiles) |
||||||
|
.ToListAsync(); |
||||||
|
|
||||||
|
List<string> all = movies.Map(m => m.MediaVersions.Head().MediaFiles.Head().Path) |
||||||
|
.Append(episodes.Map(e => e.MediaVersions.Head().MediaFiles.Head().Path)) |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
if (all.Any()) |
||||||
|
{ |
||||||
|
var paths = all.Take(5).ToList(); |
||||||
|
|
||||||
|
var files = string.Join(", ", paths); |
||||||
|
|
||||||
|
return WarningResult($"There are {all.Count} files with zero duration, including the following: {files}"); |
||||||
|
} |
||||||
|
|
||||||
|
return OkResult(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Health; |
||||||
|
using ErsatzTV.Core.Health.Checks; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Health |
||||||
|
{ |
||||||
|
public class HealthCheckService : IHealthCheckService |
||||||
|
{ |
||||||
|
private readonly List<IHealthCheck> _checks; |
||||||
|
|
||||||
|
// ReSharper disable SuggestBaseTypeForParameterInConstructor
|
||||||
|
public HealthCheckService( |
||||||
|
IFFmpegVersionHealthCheck ffmpegVersionHealthCheck, |
||||||
|
IFFmpegReportsHealthCheck fFmpegReportsHealthCheck, |
||||||
|
IHardwareAccelerationHealthCheck hardwareAccelerationHealthCheck, |
||||||
|
IMovieMetadataHealthCheck movieMetadataHealthCheck, |
||||||
|
IEpisodeMetadataHealthCheck episodeMetadataHealthCheck, |
||||||
|
IZeroDurationHealthCheck zeroDurationHealthCheck) |
||||||
|
{ |
||||||
|
_checks = new List<IHealthCheck> |
||||||
|
{ |
||||||
|
ffmpegVersionHealthCheck, |
||||||
|
fFmpegReportsHealthCheck, |
||||||
|
hardwareAccelerationHealthCheck, |
||||||
|
movieMetadataHealthCheck, |
||||||
|
episodeMetadataHealthCheck, |
||||||
|
zeroDurationHealthCheck |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<List<HealthCheckResult>> PerformHealthChecks() => |
||||||
|
_checks.Map(c => c.Check()).Sequence().Map(results => results.ToList()); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue