diff --git a/CHANGELOG.md b/CHANGELOG.md index 174c30889..52aed48b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Add artists directly to schedules - Include MPAA and VCHIP content ratings in XMLTV guide data +- Quickly skip missing files during Plex library scan ### Fixed - Ignore unsupported plex guids (this prevented some libraries from scanning correctly) diff --git a/ErsatzTV.Core/Interfaces/Plex/IPlexPathReplacementService.cs b/ErsatzTV.Core/Interfaces/Plex/IPlexPathReplacementService.cs index a44df40a3..1a3410f12 100644 --- a/ErsatzTV.Core/Interfaces/Plex/IPlexPathReplacementService.cs +++ b/ErsatzTV.Core/Interfaces/Plex/IPlexPathReplacementService.cs @@ -1,9 +1,12 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; namespace ErsatzTV.Core.Interfaces.Plex { public interface IPlexPathReplacementService { Task GetReplacementPlexPath(int libraryPathId, string path); + string GetReplacementPlexPath(List pathReplacements, string path, bool log = true); } } diff --git a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs index 54f36b142..8c07ac1bd 100644 --- a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; @@ -18,6 +19,9 @@ namespace ErsatzTV.Core.Plex { private readonly ILogger _logger; private readonly IMediator _mediator; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IPlexPathReplacementService _plexPathReplacementService; + private readonly ILocalFileSystem _localFileSystem; private readonly IMetadataRepository _metadataRepository; private readonly IMovieRepository _movieRepository; private readonly IPlexServerApiClient _plexServerApiClient; @@ -31,6 +35,9 @@ namespace ErsatzTV.Core.Plex ISearchIndex searchIndex, ISearchRepository searchRepository, IMediator mediator, + IMediaSourceRepository mediaSourceRepository, + IPlexPathReplacementService plexPathReplacementService, + ILocalFileSystem localFileSystem, ILogger logger) : base(metadataRepository, logger) { @@ -40,6 +47,9 @@ namespace ErsatzTV.Core.Plex _searchIndex = searchIndex; _searchRepository = searchRepository; _mediator = mediator; + _mediaSourceRepository = mediaSourceRepository; + _plexPathReplacementService = plexPathReplacementService; + _localFileSystem = localFileSystem; _logger = logger; } @@ -48,6 +58,9 @@ namespace ErsatzTV.Core.Plex PlexServerAuthToken token, PlexLibrary library) { + List pathReplacements = await _mediaSourceRepository + .GetPlexPathReplacements(library.MediaSourceId); + Either> entries = await _plexServerApiClient.GetMovieLibraryContents( library, connection, @@ -56,9 +69,27 @@ namespace ErsatzTV.Core.Plex await entries.Match( async movieEntries => { - foreach (PlexMovie incoming in movieEntries) + var validMovies = new List(); + foreach (PlexMovie movie in movieEntries.OrderBy(m => m.MovieMetadata.Head().Title)) { - decimal percentCompletion = (decimal) movieEntries.IndexOf(incoming) / movieEntries.Count; + string localPath = _plexPathReplacementService.GetReplacementPlexPath( + pathReplacements, + movie.MediaVersions.Head().MediaFiles.Head().Path, + false); + + if (!_localFileSystem.FileExists(localPath)) + { + _logger.LogWarning("Skipping plex movie that does not exist at {Path}", localPath); + } + else + { + validMovies.Add(movie); + } + } + + foreach (PlexMovie incoming in validMovies) + { + decimal percentCompletion = (decimal) validMovies.IndexOf(incoming) / validMovies.Count; await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion)); // TODO: figure out how to rebuild playlists @@ -92,7 +123,7 @@ namespace ErsatzTV.Core.Plex }); } - var movieKeys = movieEntries.Map(s => s.Key).ToList(); + var movieKeys = validMovies.Map(s => s.Key).ToList(); List ids = await _movieRepository.RemoveMissingPlexMovies(library, movieKeys); await _searchIndex.RemoveItems(ids); diff --git a/ErsatzTV.Core/Plex/PlexPathReplacementService.cs b/ErsatzTV.Core/Plex/PlexPathReplacementService.cs index 8e5a5eb1a..ac3cd6ce4 100644 --- a/ErsatzTV.Core/Plex/PlexPathReplacementService.cs +++ b/ErsatzTV.Core/Plex/PlexPathReplacementService.cs @@ -31,7 +31,13 @@ namespace ErsatzTV.Core.Plex { List replacements = await _mediaSourceRepository.GetPlexPathReplacementsByLibraryId(libraryPathId); - Option maybeReplacement = replacements + + return GetReplacementPlexPath(replacements, path); + } + + public string GetReplacementPlexPath(List pathReplacements, string path, bool log = true) + { + Option maybeReplacement = pathReplacements .SingleOrDefault( r => { @@ -39,6 +45,7 @@ namespace ErsatzTV.Core.Plex string prefix = r.PlexPath.EndsWith(separatorChar) ? r.PlexPath : r.PlexPath + separatorChar; return path.StartsWith(prefix); }); + return maybeReplacement.Match( replacement => { @@ -52,11 +59,15 @@ namespace ErsatzTV.Core.Plex finalPath = finalPath.Replace(@"/", @"\"); } - _logger.LogInformation( - "Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}", - replacement.PlexPath, - replacement.LocalPath, - finalPath); + if (log) + { + _logger.LogInformation( + "Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}", + replacement.PlexPath, + replacement.LocalPath, + finalPath); + } + return finalPath; }, () => path); diff --git a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs index f1d08260c..ddf5397b9 100644 --- a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; @@ -18,9 +19,12 @@ namespace ErsatzTV.Core.Plex { public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionLibraryScanner { + private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; + private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediator _mediator; private readonly IMetadataRepository _metadataRepository; + private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexServerApiClient _plexServerApiClient; private readonly ISearchIndex _searchIndex; private readonly ISearchRepository _searchRepository; @@ -33,6 +37,9 @@ namespace ErsatzTV.Core.Plex ISearchIndex searchIndex, ISearchRepository searchRepository, IMediator mediator, + IMediaSourceRepository mediaSourceRepository, + IPlexPathReplacementService plexPathReplacementService, + ILocalFileSystem localFileSystem, ILogger logger) : base(metadataRepository, logger) { @@ -42,6 +49,9 @@ namespace ErsatzTV.Core.Plex _searchIndex = searchIndex; _searchRepository = searchRepository; _mediator = mediator; + _mediaSourceRepository = mediaSourceRepository; + _plexPathReplacementService = plexPathReplacementService; + _localFileSystem = localFileSystem; _logger = logger; } @@ -50,6 +60,9 @@ namespace ErsatzTV.Core.Plex PlexServerAuthToken token, PlexLibrary library) { + List pathReplacements = await _mediaSourceRepository + .GetPlexPathReplacements(library.MediaSourceId); + Either> entries = await _plexServerApiClient.GetShowLibraryContents( library, connection, @@ -72,7 +85,7 @@ namespace ErsatzTV.Core.Plex await maybeShow.Match( async result => { - await ScanSeasons(library, result.Item, connection, token); + await ScanSeasons(library, pathReplacements, result.Item, connection, token); if (result.IsAdded) { @@ -271,13 +284,14 @@ namespace ErsatzTV.Core.Plex } private async Task> ScanSeasons( - PlexLibrary plexMediaSourceLibrary, + PlexLibrary library, + List pathReplacements, PlexShow show, PlexConnection connection, PlexServerAuthToken token) { Either> entries = await _plexServerApiClient.GetShowSeasons( - plexMediaSourceLibrary, + library, show, connection, token); @@ -291,11 +305,11 @@ namespace ErsatzTV.Core.Plex // TODO: figure out how to rebuild playlists Either maybeSeason = await _televisionRepository - .GetOrAddPlexSeason(plexMediaSourceLibrary, incoming) + .GetOrAddPlexSeason(library, incoming) .BindT(existing => UpdateMetadataAndArtwork(existing, incoming)); await maybeSeason.Match( - async season => await ScanEpisodes(plexMediaSourceLibrary, season, connection, token), + async season => await ScanEpisodes(library, pathReplacements, season, connection, token), error => { _logger.LogWarning( @@ -315,7 +329,7 @@ namespace ErsatzTV.Core.Plex { _logger.LogWarning( "Error synchronizing plex library {Path}: {Error}", - plexMediaSourceLibrary.Name, + library.Name, error.Value); return Left(error).AsTask(); @@ -355,13 +369,14 @@ namespace ErsatzTV.Core.Plex } private async Task> ScanEpisodes( - PlexLibrary plexMediaSourceLibrary, + PlexLibrary library, + List pathReplacements, PlexSeason season, PlexConnection connection, PlexServerAuthToken token) { Either> entries = await _plexServerApiClient.GetSeasonEpisodes( - plexMediaSourceLibrary, + library, season, connection, token); @@ -369,19 +384,39 @@ namespace ErsatzTV.Core.Plex return await entries.Match>>( async episodeEntries => { - foreach (PlexEpisode incoming in episodeEntries) + var validEpisodes = new List(); + foreach (PlexEpisode episode in episodeEntries) + { + string localPath = _plexPathReplacementService.GetReplacementPlexPath( + pathReplacements, + episode.MediaVersions.Head().MediaFiles.Head().Path, + false); + + if (!_localFileSystem.FileExists(localPath)) + { + _logger.LogWarning( + "Skipping plex episode that does not exist at {Path}", + localPath); + } + else + { + validEpisodes.Add(episode); + } + } + + foreach (PlexEpisode incoming in validEpisodes) { incoming.SeasonId = season.Id; // TODO: figure out how to rebuild playlists Either maybeEpisode = await _televisionRepository - .GetOrAddPlexEpisode(plexMediaSourceLibrary, incoming) + .GetOrAddPlexEpisode(library, incoming) .BindT(existing => UpdateMetadata(existing, incoming)) .BindT( existing => UpdateStatistics( existing, incoming, - plexMediaSourceLibrary, + library, connection, token)) .BindT(existing => UpdateArtwork(existing, incoming)); @@ -401,7 +436,7 @@ namespace ErsatzTV.Core.Plex }); } - var episodeKeys = episodeEntries.Map(s => s.Key).ToList(); + var episodeKeys = validEpisodes.Map(s => s.Key).ToList(); List ids = await _televisionRepository.RemoveMissingPlexEpisodes(season.Key, episodeKeys); await _searchIndex.RemoveItems(ids); _searchIndex.Commit(); @@ -412,7 +447,7 @@ namespace ErsatzTV.Core.Plex { _logger.LogWarning( "Error synchronizing plex library {Path}: {Error}", - plexMediaSourceLibrary.Name, + library.Name, error.Value); return Left(error).AsTask(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs index 33593ada1..2b397e398 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs @@ -72,6 +72,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories { using TvContext context = _dbContextFactory.CreateDbContext(); return context.PlexPathReplacements + .Include(ppr => ppr.PlexMediaSource) .Filter(r => r.PlexMediaSourceId == plexMediaSourceId) .ToListAsync(); }