From 558e8acf5f6b54698cd7afcd1b62df0f304d9f8f Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 24 Apr 2022 11:59:41 -0500 Subject: [PATCH] unavailable improvements (#756) * add unavailable health check * improve file not found health check --- .../Health/Checks/IUnavailableHealthCheck.cs | 5 ++ .../Repositories/IPlexMovieRepository.cs | 2 +- .../Repositories/IPlexTelevisionRepository.cs | 2 +- ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs | 5 +- .../Plex/PlexTelevisionLibraryScanner.cs | 5 +- .../Data/Repositories/PlexMovieRepository.cs | 24 ++++-- .../Repositories/PlexTelevisionRepository.cs | 20 +++-- .../Health/Checks/FileNotFoundHealthCheck.cs | 49 ++++------- .../Health/Checks/UnavailableHealthCheck.cs | 82 +++++++++++++++++++ .../Health/HealthCheckService.cs | 2 + ErsatzTV/Startup.cs | 1 + 11 files changed, 146 insertions(+), 51 deletions(-) create mode 100644 ErsatzTV.Core/Health/Checks/IUnavailableHealthCheck.cs create mode 100644 ErsatzTV.Infrastructure/Health/Checks/UnavailableHealthCheck.cs diff --git a/ErsatzTV.Core/Health/Checks/IUnavailableHealthCheck.cs b/ErsatzTV.Core/Health/Checks/IUnavailableHealthCheck.cs new file mode 100644 index 000000000..66e4b564c --- /dev/null +++ b/ErsatzTV.Core/Health/Checks/IUnavailableHealthCheck.cs @@ -0,0 +1,5 @@ +namespace ErsatzTV.Core.Health.Checks; + +public interface IUnavailableHealthCheck : IHealthCheck +{ +} diff --git a/ErsatzTV.Core/Interfaces/Repositories/IPlexMovieRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IPlexMovieRepository.cs index 70cf7109c..2c2210a17 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IPlexMovieRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IPlexMovieRepository.cs @@ -5,6 +5,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories; public interface IPlexMovieRepository { Task FlagNormal(PlexLibrary library, PlexMovie movie); - Task FlagUnavailable(PlexLibrary library, PlexMovie movie); + Task> FlagUnavailable(PlexLibrary library, PlexMovie movie); Task> FlagFileNotFound(PlexLibrary library, List plexMovieKeys); } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs index 6e109ce8f..9010aa7bc 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs @@ -9,7 +9,7 @@ public interface IPlexTelevisionRepository Task> GetExistingPlexSeasons(PlexLibrary library, PlexShow show); Task> GetExistingPlexEpisodes(PlexLibrary library, PlexSeason season); Task FlagNormal(PlexLibrary library, PlexEpisode episode); - Task FlagUnavailable(PlexLibrary library, PlexEpisode episode); + Task> FlagUnavailable(PlexLibrary library, PlexEpisode episode); Task> FlagFileNotFoundShows(PlexLibrary library, List plexShowKeys); Task> FlagFileNotFoundSeasons(PlexLibrary library, List plexSeasonKeys); Task> FlagFileNotFoundEpisodes(PlexLibrary library, List plexEpisodeKeys); diff --git a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs index 697d1a2ec..b03f36086 100644 --- a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs @@ -229,7 +229,10 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan { if (!_localFileSystem.FileExists(localPath)) { - await _plexMovieRepository.FlagUnavailable(library, incoming); + foreach (int id in await _plexMovieRepository.FlagUnavailable(library, incoming)) + { + await _searchIndex.RebuildItems(_searchRepository, new List { id }); + } } // _logger.LogDebug("NOOP: etag has not changed for plex movie with key {Key}", incoming.Key); diff --git a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs index b943f09b1..52e63c2cd 100644 --- a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs @@ -643,7 +643,10 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL { if (!_localFileSystem.FileExists(localPath)) { - await _plexTelevisionRepository.FlagUnavailable(library, incoming); + foreach (int id in await _plexTelevisionRepository.FlagUnavailable(library, incoming)) + { + await _searchIndex.RebuildItems(_searchRepository, new List { id }); + } } // _logger.LogDebug("NOOP: etag has not changed for plex episode with key {Key}", incoming.Key); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlexMovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlexMovieRepository.cs index 614d537d7..62768d48a 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/PlexMovieRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/PlexMovieRepository.cs @@ -26,19 +26,27 @@ public class PlexMovieRepository : IPlexMovieRepository new { LibraryId = library.Id, movie.Key }).Map(count => count > 0); } - public async Task FlagUnavailable(PlexLibrary library, PlexMovie movie) + public async Task> FlagUnavailable(PlexLibrary library, PlexMovie movie) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); movie.State = MediaItemState.Unavailable; - return await dbContext.Connection.ExecuteAsync( - @"UPDATE MediaItem SET State = 2 WHERE Id IN - (SELECT PlexMovie.Id FROM PlexMovie - INNER JOIN MediaItem MI ON MI.Id = PlexMovie.Id - INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId - WHERE PlexMovie.Key = @Key)", - new { LibraryId = library.Id, movie.Key }).Map(count => count > 0); + Option maybeId = await dbContext.Connection.ExecuteScalarAsync( + @"SELECT PlexMovie.Id FROM PlexMovie + INNER JOIN MediaItem MI ON MI.Id = PlexMovie.Id + INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId + WHERE PlexMovie.Key = @Key", + new { LibraryId = library.Id, movie.Key }); + + foreach (int id in maybeId) + { + return await dbContext.Connection.ExecuteAsync( + @"UPDATE MediaItem SET State = 2 WHERE Id = @Id", + new { Id = id }).Map(count => count > 0 ? Some(id) : None); + } + + return None; } public async Task> FlagFileNotFound(PlexLibrary library, List plexMovieKeys) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs index 20fa228a4..c5115fcbc 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs @@ -68,19 +68,27 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository new { LibraryId = library.Id, episode.Key }).Map(count => count > 0); } - public async Task FlagUnavailable(PlexLibrary library, PlexEpisode episode) + public async Task> FlagUnavailable(PlexLibrary library, PlexEpisode episode) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); episode.State = MediaItemState.Unavailable; - return await dbContext.Connection.ExecuteAsync( - @"UPDATE MediaItem SET State = 2 WHERE Id IN - (SELECT PlexEpisode.Id FROM PlexEpisode + Option maybeId = await dbContext.Connection.ExecuteScalarAsync( + @"SELECT PlexEpisode.Id FROM PlexEpisode INNER JOIN MediaItem MI ON MI.Id = PlexEpisode.Id INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId - WHERE PlexEpisode.Key = @Key)", - new { LibraryId = library.Id, episode.Key }).Map(count => count > 0); + WHERE PlexEpisode.Key = @Key", + new { LibraryId = library.Id, episode.Key }); + + foreach (int id in maybeId) + { + return await dbContext.Connection.ExecuteAsync( + @"UPDATE MediaItem SET State = 2 WHERE Id = @Id", + new { Id = id }).Map(count => count > 0 ? Some(id) : None); + } + + return None; } public async Task> FlagFileNotFoundShows(PlexLibrary library, List plexShowKeys) diff --git a/ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs index 51342b0c1..892e3cd95 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs @@ -20,51 +20,34 @@ public class FileNotFoundHealthCheck : BaseHealthCheck, IFileNotFoundHealthCheck { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - List episodes = await dbContext.Episodes - .Filter(e => e.State == MediaItemState.FileNotFound) - .Include(e => e.MediaVersions) + IQueryable mediaItems = dbContext.MediaItems + .Filter(mi => mi.State == MediaItemState.FileNotFound) + .Include(mi => (mi as Episode).MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(cancellationToken); - - List movies = await dbContext.Movies - .Filter(m => m.State == MediaItemState.FileNotFound) - .Include(m => m.MediaVersions) + .Include(mi => (mi as Movie).MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(cancellationToken); - - List musicVideos = await dbContext.MusicVideos - .Filter(mv => mv.State == MediaItemState.FileNotFound) - .Include(mv => mv.MediaVersions) + .Include(mi => (mi as MusicVideo).MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(cancellationToken); - - List otherVideos = await dbContext.OtherVideos - .Filter(ov => ov.State == MediaItemState.FileNotFound) - .Include(ov => ov.MediaVersions) + .Include(mi => (mi as OtherVideo).MediaVersions) .ThenInclude(mv => mv.MediaFiles) - .ToListAsync(cancellationToken); + .Include(mi => (mi as Song).MediaVersions) + .ThenInclude(mv => mv.MediaFiles); - List songs = await dbContext.Songs - .Filter(s => s.State == MediaItemState.FileNotFound) - .Include(s => s.MediaVersions) - .ThenInclude(mv => mv.MediaFiles) + List five = await mediaItems + .OrderBy(mi => mi.Id) + .Take(5) .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)) - .Append(otherVideos.Map(ov => ov.GetHeadVersion().MediaFiles.Head().Path)) - .Append(songs.Map(s => s.GetHeadVersion().MediaFiles.Head().Path)) - .ToList(); - - if (all.Any()) + if (five.Any()) { - var paths = all.Take(5).ToList(); + IEnumerable paths = five.Map(mi => mi.GetHeadVersion().MediaFiles.Head().Path); var files = string.Join(", ", paths); + int count = await mediaItems.CountAsync(cancellationToken); + return WarningResult( - $"There are {all.Count} files that do not exist on disk, including the following: {files}", + $"There are {count} files that do not exist on disk, including the following: {files}", "/media/trash"); } diff --git a/ErsatzTV.Infrastructure/Health/Checks/UnavailableHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/UnavailableHealthCheck.cs new file mode 100644 index 000000000..4a48f5b47 --- /dev/null +++ b/ErsatzTV.Infrastructure/Health/Checks/UnavailableHealthCheck.cs @@ -0,0 +1,82 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; +using ErsatzTV.Core.Health; +using ErsatzTV.Core.Health.Checks; +using ErsatzTV.Core.Interfaces.Emby; +using ErsatzTV.Core.Interfaces.Jellyfin; +using ErsatzTV.Core.Interfaces.Plex; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Infrastructure.Health.Checks; + +public class UnavailableHealthCheck : BaseHealthCheck, IUnavailableHealthCheck +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly IEmbyPathReplacementService _embyPathReplacementService; + private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; + private readonly IPlexPathReplacementService _plexPathReplacementService; + + public UnavailableHealthCheck( + IDbContextFactory dbContextFactory, + IPlexPathReplacementService plexPathReplacementService, + IJellyfinPathReplacementService jellyfinPathReplacementService, + IEmbyPathReplacementService embyPathReplacementService) + { + _dbContextFactory = dbContextFactory; + _plexPathReplacementService = plexPathReplacementService; + _jellyfinPathReplacementService = jellyfinPathReplacementService; + _embyPathReplacementService = embyPathReplacementService; + } + + protected override string Title => "Unavailable"; + + public async Task Check(CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + IQueryable mediaItems = dbContext.MediaItems + .Filter(mi => mi.State == MediaItemState.Unavailable) + .Include(mi => (mi as Episode).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(mi => (mi as Movie).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(mi => (mi as MusicVideo).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(mi => (mi as OtherVideo).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(mi => (mi as Song).MediaVersions) + .ThenInclude(mv => mv.MediaFiles); + + List five = await mediaItems + .OrderBy(mi => mi.Id) + .Take(5) + .ToListAsync(cancellationToken); + + if (five.Any()) + { + var paths = new List(); + + foreach (MediaItem mediaItem in five) + { + string path = await mediaItem.GetLocalPath( + _plexPathReplacementService, + _jellyfinPathReplacementService, + _embyPathReplacementService, + false); + + paths.Add(path); + } + + var files = string.Join(", ", paths); + + int count = await mediaItems.CountAsync(cancellationToken); + + return WarningResult( + $"There are {count} files that are unavailable because ErsatzTV cannot find them on disk, including the following: {files}", + "/search?query=state%3aUnavailable"); + } + + return OkResult(); + } +} diff --git a/ErsatzTV.Infrastructure/Health/HealthCheckService.cs b/ErsatzTV.Infrastructure/Health/HealthCheckService.cs index 805df215f..a557860a6 100644 --- a/ErsatzTV.Infrastructure/Health/HealthCheckService.cs +++ b/ErsatzTV.Infrastructure/Health/HealthCheckService.cs @@ -15,6 +15,7 @@ public class HealthCheckService : IHealthCheckService IEpisodeMetadataHealthCheck episodeMetadataHealthCheck, IZeroDurationHealthCheck zeroDurationHealthCheck, IFileNotFoundHealthCheck fileNotFoundHealthCheck, + IUnavailableHealthCheck unavailableHealthCheck, IVaapiDriverHealthCheck vaapiDriverHealthCheck, IErrorReportsHealthCheck errorReportsHealthCheck) => _checks = new List @@ -26,6 +27,7 @@ public class HealthCheckService : IHealthCheckService episodeMetadataHealthCheck, zeroDurationHealthCheck, fileNotFoundHealthCheck, + unavailableHealthCheck, vaapiDriverHealthCheck, errorReportsHealthCheck }; diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 7d2f099bc..8f13c17ce 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -336,6 +336,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();