Browse Source

add some health checks to home page (#374)

pull/375/head
Jason Dove 5 years ago committed by GitHub
parent
commit
1eb51ad2f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      CHANGELOG.md
  2. 8
      ErsatzTV.Application/Health/Queries/GetAllHealthCheckResults.cs
  3. 21
      ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs
  4. 6
      ErsatzTV.Core/Health/Checks/IEpisodeMetadataHealthCheck.cs
  5. 6
      ErsatzTV.Core/Health/Checks/IFFmpegReportsHealthCheck.cs
  6. 6
      ErsatzTV.Core/Health/Checks/IFFmpegVersionHealthCheck.cs
  7. 6
      ErsatzTV.Core/Health/Checks/IHardwareAccelerationHealthCheck.cs
  8. 6
      ErsatzTV.Core/Health/Checks/IMovieMetadataHealthCheck.cs
  9. 6
      ErsatzTV.Core/Health/Checks/IZeroDurationHealthCheck.cs
  10. 4
      ErsatzTV.Core/Health/HealthCheckResult.cs
  11. 10
      ErsatzTV.Core/Health/HealthCheckStatus.cs
  12. 9
      ErsatzTV.Core/Health/IHealthCheck.cs
  13. 10
      ErsatzTV.Core/Health/IHealthCheckService.cs
  14. 52
      ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs
  15. 53
      ErsatzTV.Infrastructure/Health/Checks/EpisodeMetadataHealthCheck.cs
  16. 37
      ErsatzTV.Infrastructure/Health/Checks/FFmpegReportsHealthCheck.cs
  17. 110
      ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs
  18. 127
      ErsatzTV.Infrastructure/Health/Checks/HardwareAccelerationHealthCheck.cs
  19. 53
      ErsatzTV.Infrastructure/Health/Checks/MovieMetadataHealthCheck.cs
  20. 54
      ErsatzTV.Infrastructure/Health/Checks/ZeroDurationHealthCheck.cs
  21. 37
      ErsatzTV.Infrastructure/Health/HealthCheckService.cs
  22. 52
      ErsatzTV/Pages/Index.razor
  23. 12
      ErsatzTV/Startup.cs

11
CHANGELOG.md

@ -4,8 +4,17 @@ All notable changes to this project will be documented in this file. @@ -4,8 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Add `Health Checks` table to home page to identify and surface common misconfigurations
- `FFmpeg Version` checks `ffmpeg` and `ffprobe` versions
- `FFmpeg Reports` checks whether ffmpeg troubleshooting reports are enabled since they can use a lot of disk space over time
- `Hardware Acceleration` checks whether channels that transcode are using acceleration methods that ffmpeg claims to support
- `Movie Metadata` checks whether all movies have metadata (fallback metadata counts as metadata)
- `Episode Metadata` checks whether all episodes have metadata (fallback metadata counts as metadata)
- `Zero Duration` checks whether all movies and episodes have a valid (non-zero) duration
### Fixed
- Fix scanning and indexing local movies and episodes with no metadata
- Fix scanning and indexing local movies and episodes without NFO metadata
- Fix displaying seasons for shows with no year (in metadata or in folder name)
## [0.0.58-alpha] - 2021-09-15

8
ErsatzTV.Application/Health/Queries/GetAllHealthCheckResults.cs

@ -0,0 +1,8 @@ @@ -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>>;
}

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

@ -0,0 +1,21 @@ @@ -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();
}
}

6
ErsatzTV.Core/Health/Checks/IEpisodeMetadataHealthCheck.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Health.Checks
{
public interface IEpisodeMetadataHealthCheck : IHealthCheck
{
}
}

6
ErsatzTV.Core/Health/Checks/IFFmpegReportsHealthCheck.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Health.Checks
{
public interface IFFmpegReportsHealthCheck : IHealthCheck
{
}
}

6
ErsatzTV.Core/Health/Checks/IFFmpegVersionHealthCheck.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Health.Checks
{
public interface IFFmpegVersionHealthCheck : IHealthCheck
{
}
}

6
ErsatzTV.Core/Health/Checks/IHardwareAccelerationHealthCheck.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Health.Checks
{
public interface IHardwareAccelerationHealthCheck : IHealthCheck
{
}
}

6
ErsatzTV.Core/Health/Checks/IMovieMetadataHealthCheck.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Health.Checks
{
public interface IMovieMetadataHealthCheck : IHealthCheck
{
}
}

6
ErsatzTV.Core/Health/Checks/IZeroDurationHealthCheck.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Health.Checks
{
public interface IZeroDurationHealthCheck : IHealthCheck
{
}
}

4
ErsatzTV.Core/Health/HealthCheckResult.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Core.Health
{
public record HealthCheckResult(string Title, HealthCheckStatus Status, string Message);
}

10
ErsatzTV.Core/Health/HealthCheckStatus.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Health
{
public enum HealthCheckStatus
{
Pass,
Fail,
Warning,
Info
}
}

9
ErsatzTV.Core/Health/IHealthCheck.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace ErsatzTV.Core.Health
{
public interface IHealthCheck
{
Task<HealthCheckResult> Check();
}
}

10
ErsatzTV.Core/Health/IHealthCheckService.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ErsatzTV.Core.Health
{
public interface IHealthCheckService
{
Task<List<HealthCheckResult>> PerformHealthChecks();
}
}

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

@ -0,0 +1,52 @@ @@ -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;
}
}
}

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

@ -0,0 +1,53 @@ @@ -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();
}
}
}

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

@ -0,0 +1,37 @@ @@ -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";
}
}

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

@ -0,0 +1,110 @@ @@ -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";
}
}

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

@ -0,0 +1,127 @@ @@ -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";
}
}

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

@ -0,0 +1,53 @@ @@ -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();
}
}
}

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

@ -0,0 +1,54 @@ @@ -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();
}
}
}

37
ErsatzTV.Infrastructure/Health/HealthCheckService.cs

@ -0,0 +1,37 @@ @@ -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());
}
}

52
ErsatzTV/Pages/Index.razor

@ -1,9 +1,12 @@ @@ -1,9 +1,12 @@
@page "/"
@using Microsoft.Extensions.Caching.Memory
@using System.Reflection
@using ErsatzTV.Application.Health.Queries
@using ErsatzTV.Core.Health
@using ErsatzTV.Core.Interfaces.GitHub
@inject IGitHubApiClient _gitHubApiClient
@inject IMemoryCache _memoryCache
@inject IMediator _mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudCard>
@ -12,11 +15,49 @@ @@ -12,11 +15,49 @@
</MudCardContent>
</MudCard>
<MudText Class="mt-6">Full changelog is available on <MudLink Href="https://github.com/jasongdove/ErsatzTV/blob/main/CHANGELOG.md">GitHub</MudLink></MudText>
<MudTable Class="mt-6"
Hover="true"
Dense="true"
ServerData="@(new Func<TableState, Task<TableData<HealthCheckResult>>>(ServerReload))"
@ref="_table">
<ToolBarContent>
<MudText Typo="Typo.h6">Health Checks</MudText>
</ToolBarContent>
<HeaderContent>
<MudTh>Check</MudTh>
<MudTh>Message</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Check">
<div style="display: flex; flex-direction: row; align-items: center">
@if (context.Status == HealthCheckStatus.Fail)
{
<MudIcon Color="@Color.Error" Icon="@Icons.Material.Filled.Error"/>
}
else if (context.Status == HealthCheckStatus.Warning)
{
<MudIcon Color="@Color.Warning" Icon="@Icons.Material.Filled.Warning"/>
}
else if (context.Status == HealthCheckStatus.Info)
{
<MudIcon Color="@Color.Info" Icon="@Icons.Material.Filled.Info"/>
}
else
{
<MudIcon Color="@Color.Success" Icon="@Icons.Material.Filled.Check"/>
}
<div class="ml-2">@context.Title</div>
</div>
</MudTd>
<MudTd DataLabel="Message">@context.Message</MudTd>
</RowTemplate>
</MudTable>
</MudContainer>
@code {
private string _releaseNotes;
private MudTable<HealthCheckResult> _table;
protected override async Task OnParametersSetAsync()
{
@ -66,5 +107,16 @@ @@ -66,5 +107,16 @@
// ignore
}
}
private async Task<TableData<HealthCheckResult>> ServerReload(TableState state)
{
List<HealthCheckResult> healthCheckResults = await _mediator.Send(new GetAllHealthCheckResults());
return new TableData<HealthCheckResult>
{
TotalItems = healthCheckResults.Count,
Items = healthCheckResults
};
}
}

12
ErsatzTV/Startup.cs

@ -11,6 +11,8 @@ using ErsatzTV.Application.Logs.Queries; @@ -11,6 +11,8 @@ using ErsatzTV.Application.Logs.Queries;
using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Health;
using ErsatzTV.Core.Health.Checks;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.GitHub;
@ -34,6 +36,8 @@ using ErsatzTV.Infrastructure.Data; @@ -34,6 +36,8 @@ using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Emby;
using ErsatzTV.Infrastructure.GitHub;
using ErsatzTV.Infrastructure.Health;
using ErsatzTV.Infrastructure.Health.Checks;
using ErsatzTV.Infrastructure.Images;
using ErsatzTV.Infrastructure.Jellyfin;
using ErsatzTV.Infrastructure.Locking;
@ -197,6 +201,14 @@ namespace ErsatzTV @@ -197,6 +201,14 @@ namespace ErsatzTV
AddChannel<IPlexBackgroundServiceRequest>(services);
AddChannel<IJellyfinBackgroundServiceRequest>(services);
AddChannel<IEmbyBackgroundServiceRequest>(services);
services.AddScoped<IFFmpegVersionHealthCheck, FFmpegVersionHealthCheck>();
services.AddScoped<IFFmpegReportsHealthCheck, FFmpegReportsHealthCheck>();
services.AddScoped<IHardwareAccelerationHealthCheck, HardwareAccelerationHealthCheck>();
services.AddScoped<IMovieMetadataHealthCheck, MovieMetadataHealthCheck>();
services.AddScoped<IEpisodeMetadataHealthCheck, EpisodeMetadataHealthCheck>();
services.AddScoped<IZeroDurationHealthCheck, ZeroDurationHealthCheck>();
services.AddScoped<IHealthCheckService, HealthCheckService>();
services.AddScoped<IChannelRepository, ChannelRepository>();
services.AddScoped<IFFmpegProfileRepository, FFmpegProfileRepository>();

Loading…
Cancel
Save