From d879ce0d0db35fef31984cb2efc4908f2781d70b Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 24 Apr 2022 13:43:06 -0500 Subject: [PATCH] bug fixes (#757) * fix docker blur hash generation * scanner async cleanup * catch and log some unauthorized exception errors --- CHANGELOG.md | 1 + .../Emby/EmbyTelevisionLibraryScanner.cs | 191 +++++------ ErsatzTV.Core/Metadata/LocalFileSystem.cs | 12 + ErsatzTV.Core/Metadata/MovieFolderScanner.cs | 96 +++--- .../Metadata/TelevisionFolderScanner.cs | 212 ++++++------ ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs | 316 +++++++++--------- .../Plex/PlexTelevisionLibraryScanner.cs | 138 ++++---- docker/Dockerfile | 2 +- docker/nvidia/ffmpeg.Dockerfile | 2 +- docker/vaapi/ffmpeg.Dockerfile | 1 + 10 files changed, 482 insertions(+), 489 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a770c907..2be6e0f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Cleanly stop Plex library scan when service termination is requested - Fix bug introduced with 0.5.2-beta that prevented some Plex content from being played - Fix spammy subtitle error message +- Fix generating blur hashes for song backgrounds in Docker ### Changed - No longer remove Plex movies and episodes from ErsatzTV when they do not exist on disk diff --git a/ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs b/ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs index 5763390b6..293aa499e 100644 --- a/ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs @@ -70,43 +70,41 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner apiKey, library.ItemId); - await maybeShows.Match( - async shows => - { - await ProcessShows( - address, - apiKey, - library, - ffmpegPath, - ffprobePath, - pathReplacements, - existingShows, - shows); - - var incomingShowIds = shows.Map(s => s.ItemId).ToList(); - var showIds = existingShows - .Filter(i => !incomingShowIds.Contains(i.ItemId)) - .Map(m => m.ItemId) - .ToList(); - List missingShowIds = await _televisionRepository.RemoveMissingShows(library, showIds); - await _searchIndex.RemoveItems(missingShowIds); - - await _televisionRepository.DeleteEmptySeasons(library); - List emptyShowIds = await _televisionRepository.DeleteEmptyShows(library); - await _searchIndex.RemoveItems(emptyShowIds); - - await _mediator.Publish(new LibraryScanProgress(library.Id, 0)); - _searchIndex.Commit(); - }, - error => - { - _logger.LogWarning( - "Error synchronizing emby library {Path}: {Error}", - library.Name, - error.Value); + foreach (BaseError error in maybeShows.LeftToSeq()) + { + _logger.LogWarning( + "Error synchronizing emby library {Path}: {Error}", + library.Name, + error.Value); + } - return Task.CompletedTask; - }); + foreach (List shows in maybeShows.RightToSeq()) + { + await ProcessShows( + address, + apiKey, + library, + ffmpegPath, + ffprobePath, + pathReplacements, + existingShows, + shows); + + var incomingShowIds = shows.Map(s => s.ItemId).ToList(); + var showIds = existingShows + .Filter(i => !incomingShowIds.Contains(i.ItemId)) + .Map(m => m.ItemId) + .ToList(); + List missingShowIds = await _televisionRepository.RemoveMissingShows(library, showIds); + await _searchIndex.RemoveItems(missingShowIds); + + await _televisionRepository.DeleteEmptySeasons(library); + List emptyShowIds = await _televisionRepository.DeleteEmptyShows(library); + await _searchIndex.RemoveItems(emptyShowIds); + + await _mediator.Publish(new LibraryScanProgress(library.Id, 0)); + _searchIndex.Commit(); + } return Unit.Default; } @@ -128,39 +126,35 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion)); Option maybeExisting = existingShows.Find(ie => ie.ItemId == incoming.ItemId); - await maybeExisting.Match( - async existing => - { - if (existing.Etag == incoming.Etag) - { - return; - } + if (maybeExisting.IsNone) + { + incoming.LibraryPathId = library.Paths.Head().Id; - _logger.LogDebug( - "UPDATE: Etag has changed for show {Show}", - incoming.ShowMetadata.Head().Title); + // _logger.LogDebug("INSERT: Item id is new for show {Show}", incoming.ShowMetadata.Head().Title); - incoming.LibraryPathId = library.Paths.Head().Id; + if (await _televisionRepository.AddShow(incoming)) + { + await _searchIndex.AddItems(_searchRepository, new List { incoming }); + } + } - Option updated = await _televisionRepository.Update(incoming); - if (updated.IsSome) - { - await _searchIndex.UpdateItems( - _searchRepository, - new List { updated.ValueUnsafe() }); - } - }, - async () => + foreach (EmbyItemEtag existing in maybeExisting) + { + if (existing.Etag == incoming.Etag) { - incoming.LibraryPathId = library.Paths.Head().Id; + return; + } - // _logger.LogDebug("INSERT: Item id is new for show {Show}", incoming.ShowMetadata.Head().Title); + _logger.LogDebug("UPDATE: Etag has changed for show {Show}", incoming.ShowMetadata.Head().Title); - if (await _televisionRepository.AddShow(incoming)) - { - await _searchIndex.AddItems(_searchRepository, new List { incoming }); - } - }); + incoming.LibraryPathId = library.Paths.Head().Id; + + Option updated = await _televisionRepository.Update(incoming); + if (updated.IsSome) + { + await _searchIndex.UpdateItems(_searchRepository, new List { updated.ValueUnsafe() }); + } + } List existingSeasons = await _televisionRepository.GetExistingSeasons(library, incoming.ItemId); @@ -168,36 +162,34 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner Either> maybeSeasons = await _embyApiClient.GetSeasonLibraryItems(address, apiKey, incoming.ItemId); - await maybeSeasons.Match( - async seasons => - { - await ProcessSeasons( - address, - apiKey, - library, - ffmpegPath, - ffprobePath, - pathReplacements, - incoming, - existingSeasons, - seasons); + foreach (BaseError error in maybeSeasons.LeftToSeq()) + { + _logger.LogWarning( + "Error synchronizing emby library {Path}: {Error}", + library.Name, + error.Value); + } - var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList(); - var seasonIds = existingSeasons - .Filter(i => !incomingSeasonIds.Contains(i.ItemId)) - .Map(m => m.ItemId) - .ToList(); - await _televisionRepository.RemoveMissingSeasons(library, seasonIds); - }, - error => - { - _logger.LogWarning( - "Error synchronizing emby library {Path}: {Error}", - library.Name, - error.Value); + foreach (List seasons in maybeSeasons.RightToSeq()) + { + await ProcessSeasons( + address, + apiKey, + library, + ffmpegPath, + ffprobePath, + pathReplacements, + incoming, + existingSeasons, + seasons); - return Task.CompletedTask; - }); + var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList(); + var seasonIds = existingSeasons + .Filter(i => !incomingSeasonIds.Contains(i.ItemId)) + .Map(m => m.ItemId) + .ToList(); + await _televisionRepository.RemoveMissingSeasons(library, seasonIds); + } } } @@ -237,9 +229,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner foreach (MediaItem toIndex in await _searchRepository.GetItemToIndex(updated.Id)) { - await _searchIndex.UpdateItems( - _searchRepository, - new List { toIndex }); + await _searchIndex.UpdateItems(_searchRepository, new List { toIndex }); } } }, @@ -361,9 +351,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner Option maybeUpdated = await _televisionRepository.Update(incoming); foreach (EmbyEpisode updated in maybeUpdated) { - await _searchIndex.UpdateItems( - _searchRepository, - new List { updated }); + await _searchIndex.UpdateItems(_searchRepository, new List { updated }); incomingEpisode = updated; } @@ -425,13 +413,14 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner refreshResult = await UpdateSubtitles(incomingEpisode, localPath); } - refreshResult.Match( - _ => { }, - error => _logger.LogWarning( + foreach (BaseError error in refreshResult.LeftToSeq()) + { + _logger.LogWarning( "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", "Statistics", localPath, - error.Value)); + error.Value); + } } } } diff --git a/ErsatzTV.Core/Metadata/LocalFileSystem.cs b/ErsatzTV.Core/Metadata/LocalFileSystem.cs index 004244ad1..1a0f52c6b 100644 --- a/ErsatzTV.Core/Metadata/LocalFileSystem.cs +++ b/ErsatzTV.Core/Metadata/LocalFileSystem.cs @@ -47,6 +47,10 @@ public class LocalFileSystem : ILocalFileSystem { return Directory.EnumerateDirectories(folder); } + catch (UnauthorizedAccessException) + { + _logger.LogWarning("Unauthorized access exception listing subdirectories of folder {Folder}", folder); + } catch (Exception ex) { // do nothing @@ -65,6 +69,10 @@ public class LocalFileSystem : ILocalFileSystem { return Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly); } + catch (UnauthorizedAccessException) + { + _logger.LogWarning("Unauthorized access exception listing files in folder {Folder}", folder); + } catch (Exception ex) { // do nothing @@ -83,6 +91,10 @@ public class LocalFileSystem : ILocalFileSystem { return Directory.EnumerateFiles(folder, searchPattern, SearchOption.TopDirectoryOnly); } + catch (UnauthorizedAccessException) + { + _logger.LogWarning("Unauthorized access exception listing files in folder {Folder}", folder); + } catch (Exception ex) { // do nothing diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs index d8a748610..2fef49f2b 100644 --- a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -88,7 +88,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner { decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); await _mediator.Publish( - new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread)); + new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread), + cancellationToken); string movieFolder = folderQueue.Dequeue(); foldersCompleted++; @@ -142,25 +143,24 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner .BindT(UpdateSubtitles) .BindT(FlagNormal); - await maybeMovie.Match( - async result => + foreach (BaseError error in maybeMovie.LeftToSeq()) + { + _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value); + } + + foreach (MediaItemScanResult result in maybeMovie.RightToSeq()) + { + if (result.IsAdded) { - if (result.IsAdded) - { - await _searchIndex.AddItems(_searchRepository, new List { result.Item }); - } - else if (result.IsUpdated) - { - await _searchIndex.UpdateItems(_searchRepository, new List { result.Item }); - } - - await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag); - }, - error => + await _searchIndex.AddItems(_searchRepository, new List { result.Item }); + } + else if (result.IsUpdated) { - _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value); - return Task.CompletedTask; - }); + await _searchIndex.UpdateItems(_searchRepository, new List { result.Item }); + } + + await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag); + } } } @@ -192,35 +192,37 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner try { Movie movie = result.Item; - await LocateNfoFile(movie).Match( - async nfoFile => + + Option maybeNfoFile = LocateNfoFile(movie); + if (maybeNfoFile.IsNone) + { + if (!Optional(movie.MovieMetadata).Flatten().Any()) { - bool shouldUpdate = Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match( - m => m.MetadataKind == MetadataKind.Fallback || - m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile), - true); - - if (shouldUpdate) + string path = movie.MediaVersions.Head().MediaFiles.Head().Path; + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); + if (await _localMetadataProvider.RefreshFallbackMetadata(movie)) { - _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); - if (await _localMetadataProvider.RefreshSidecarMetadata(movie, nfoFile)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } - }, - async () => + } + } + + foreach (string nfoFile in maybeNfoFile) + { + bool shouldUpdate = Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match( + m => m.MetadataKind == MetadataKind.Fallback || + m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile), + true); + + if (shouldUpdate) { - if (!Optional(movie.MovieMetadata).Flatten().Any()) + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); + if (await _localMetadataProvider.RefreshSidecarMetadata(movie, nfoFile)) { - string path = movie.MediaVersions.Head().MediaFiles.Head().Path; - _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); - if (await _localMetadataProvider.RefreshFallbackMetadata(movie)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } - }); + } + } return result; } @@ -239,12 +241,12 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner try { Movie movie = result.Item; - await LocateArtwork(movie, artworkKind).IfSomeAsync( - async posterFile => - { - MovieMetadata metadata = movie.MovieMetadata.Head(); - await RefreshArtwork(posterFile, metadata, artworkKind, None, None, cancellationToken); - }); + Option maybeArtwork = LocateArtwork(movie, artworkKind); + foreach (string posterFile in maybeArtwork) + { + MovieMetadata metadata = movie.MovieMetadata.Head(); + await RefreshArtwork(posterFile, metadata, artworkKind, None, None, cancellationToken); + } return result; } diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index 5e5f0ed3c..780551afd 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -83,7 +83,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan { decimal percentCompletion = (decimal)allShowFolders.IndexOf(showFolder) / allShowFolders.Count; await _mediator.Publish( - new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread)); + new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread), + cancellationToken); Either> maybeShow = await FindOrCreateShow(libraryPath.Id, showFolder) @@ -92,34 +93,27 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.FanArt, cancellationToken)) .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Thumbnail, cancellationToken)); - await maybeShow.Match( - async result => - { - await ScanSeasons( - libraryPath, - ffmpegPath, - ffprobePath, - result.Item, - showFolder, - cancellationToken); + foreach (BaseError error in maybeShow.LeftToSeq()) + { + _logger.LogWarning( + "Error processing show in folder {Folder}: {Error}", + showFolder, + error.Value); + } - if (result.IsAdded) - { - await _searchIndex.AddItems(_searchRepository, new List { result.Item }); - } - else if (result.IsUpdated) - { - await _searchIndex.UpdateItems(_searchRepository, new List { result.Item }); - } - }, - error => + foreach (MediaItemScanResult result in maybeShow.RightToSeq()) + { + await ScanSeasons(libraryPath, ffmpegPath, ffprobePath, result.Item, showFolder, cancellationToken); + + if (result.IsAdded) { - _logger.LogWarning( - "Error processing show in folder {Folder}: {Error}", - showFolder, - error.Value); - return Task.FromResult(Unit.Default); - }); + await _searchIndex.AddItems(_searchRepository, new List { result.Item }); + } + else if (result.IsUpdated) + { + await _searchIndex.UpdateItems(_searchRepository, new List { result.Item }); + } + } } foreach (string path in await _televisionRepository.FindEpisodePaths(libraryPath)) @@ -249,16 +243,15 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan .BindT(e => FlagNormal(new MediaItemScanResult(e))) .MapT(r => r.Item); - await maybeEpisode.Match( - async episode => - { - await _searchIndex.UpdateItems(_searchRepository, new List { episode }); - }, - error => - { - _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value); - return Task.CompletedTask; - }); + foreach (BaseError error in maybeEpisode.LeftToSeq()) + { + _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value); + } + + foreach (Episode episode in maybeEpisode.RightToSeq()) + { + await _searchIndex.UpdateItems(_searchRepository, new List { episode }); + } } // TODO: remove missing episodes? @@ -273,34 +266,36 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan try { Show show = result.Item; - await LocateNfoFileForShow(showFolder).Match( - async nfoFile => + + Option maybeNfo = LocateNfoFileForShow(showFolder); + if (maybeNfo.IsNone) + { + if (!Optional(show.ShowMetadata).Flatten().Any()) { - bool shouldUpdate = Optional(show.ShowMetadata).Flatten().HeadOrNone().Match( - m => m.MetadataKind == MetadataKind.Fallback || - m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile), - true); - - if (shouldUpdate) + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", showFolder); + if (await _localMetadataProvider.RefreshFallbackMetadata(show, showFolder)) { - _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); - if (await _localMetadataProvider.RefreshSidecarMetadata(show, nfoFile)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } - }, - async () => + } + } + + foreach (string nfoFile in maybeNfo) + { + bool shouldUpdate = Optional(show.ShowMetadata).Flatten().HeadOrNone().Match( + m => m.MetadataKind == MetadataKind.Fallback || + m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile), + true); + + if (shouldUpdate) { - if (!Optional(show.ShowMetadata).Flatten().Any()) + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); + if (await _localMetadataProvider.RefreshSidecarMetadata(show, nfoFile)) { - _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", showFolder); - if (await _localMetadataProvider.RefreshFallbackMetadata(show, showFolder)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } - }); + } + } return result; } @@ -337,33 +332,34 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan { try { - await LocateNfoFile(episode).Match( - async nfoFile => - { - bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match( - m => m.MetadataKind == MetadataKind.Fallback || - m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile), - true); + Option maybeNfo = LocateNfoFile(episode); + if (maybeNfo.IsNone) + { + bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match( + m => m.DateUpdated == SystemTime.MinValueUtc, + true); - if (shouldUpdate) - { - _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); - await _localMetadataProvider.RefreshSidecarMetadata(episode, nfoFile); - } - }, - async () => + if (shouldUpdate) { - bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match( - m => m.DateUpdated == SystemTime.MinValueUtc, - true); + string path = episode.MediaVersions.Head().MediaFiles.Head().Path; + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); + await _localMetadataProvider.RefreshFallbackMetadata(episode); + } + } - if (shouldUpdate) - { - string path = episode.MediaVersions.Head().MediaFiles.Head().Path; - _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); - await _localMetadataProvider.RefreshFallbackMetadata(episode); - } - }); + foreach (string nfoFile in maybeNfo) + { + bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match( + m => m.MetadataKind == MetadataKind.Fallback || + m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile), + true); + + if (shouldUpdate) + { + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); + await _localMetadataProvider.RefreshSidecarMetadata(episode, nfoFile); + } + } return episode; } @@ -383,12 +379,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan try { Show show = result.Item; - await LocateArtworkForShow(showFolder, artworkKind).IfSomeAsync( - async artworkFile => - { - ShowMetadata metadata = show.ShowMetadata.Head(); - await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); - }); + Option maybeArtwork = LocateArtworkForShow(showFolder, artworkKind); + foreach (string artworkFile in maybeArtwork) + { + ShowMetadata metadata = show.ShowMetadata.Head(); + await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); + } return result; } @@ -406,12 +402,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan { try { - await LocatePoster(season, seasonFolder).IfSomeAsync( - async posterFile => - { - SeasonMetadata metadata = season.SeasonMetadata.Head(); - await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster, None, None, cancellationToken); - }); + Option maybePoster = LocatePoster(season, seasonFolder); + foreach (string posterFile in maybePoster) + { + SeasonMetadata metadata = season.SeasonMetadata.Head(); + await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster, None, None, cancellationToken); + } return season; } @@ -426,20 +422,20 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan { try { - await LocateThumbnail(episode).IfSomeAsync( - async posterFile => + Option maybeThumbnail = LocateThumbnail(episode); + foreach (string thumbnailFile in maybeThumbnail) + { + foreach (EpisodeMetadata metadata in episode.EpisodeMetadata) { - foreach (EpisodeMetadata metadata in episode.EpisodeMetadata) - { - await RefreshArtwork( - posterFile, - metadata, - ArtworkKind.Thumbnail, - None, - None, - cancellationToken); - } - }); + await RefreshArtwork( + thumbnailFile, + metadata, + ArtworkKind.Thumbnail, + None, + None, + cancellationToken); + } + } return episode; } diff --git a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs index b03f36086..140c0ae05 100644 --- a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs @@ -290,28 +290,26 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan Either refreshResult = await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, existing, localPath); - await refreshResult.Match( - async _ => - { - foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id)) - { - await _searchIndex.UpdateItems( - _searchRepository, - new List { updated }); - } - - await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, incomingVersion); - }, - error => + foreach (BaseError error in refreshResult.LeftToSeq()) + { + _logger.LogWarning( + "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", + "Statistics", + localPath, + error.Value); + } + + foreach (bool _ in refreshResult.RightToSeq()) + { + foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id)) { - _logger.LogWarning( - "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", - "Statistics", - localPath, - error.Value); - - return Task.CompletedTask; - }); + await _searchIndex.UpdateItems( + _searchRepository, + new List { updated }); + } + + await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, incomingVersion); + } } } @@ -340,193 +338,191 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan connection, token); - await maybeMetadata.Match( - async fullMetadata => + foreach (MovieMetadata fullMetadata in maybeMetadata.RightToSeq()) + { + if (existingMetadata.MetadataKind != MetadataKind.External) { - if (existingMetadata.MetadataKind != MetadataKind.External) - { - existingMetadata.MetadataKind = MetadataKind.External; - await _metadataRepository.MarkAsExternal(existingMetadata); - } + existingMetadata.MetadataKind = MetadataKind.External; + await _metadataRepository.MarkAsExternal(existingMetadata); + } - if (existingMetadata.ContentRating != fullMetadata.ContentRating) - { - existingMetadata.ContentRating = fullMetadata.ContentRating; - await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating); - result.IsUpdated = true; - } + if (existingMetadata.ContentRating != fullMetadata.ContentRating) + { + existingMetadata.ContentRating = fullMetadata.ContentRating; + await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating); + result.IsUpdated = true; + } - foreach (Genre genre in existingMetadata.Genres - .Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name)) - .ToList()) + foreach (Genre genre in existingMetadata.Genres + .Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Genres.Remove(genre); + if (await _metadataRepository.RemoveGenre(genre)) { - existingMetadata.Genres.Remove(genre); - if (await _metadataRepository.RemoveGenre(genre)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Genre genre in fullMetadata.Genres - .Filter(g => existingMetadata.Genres.All(g2 => g2.Name != g.Name)) - .ToList()) + foreach (Genre genre in fullMetadata.Genres + .Filter(g => existingMetadata.Genres.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Genres.Add(genre); + if (await _movieRepository.AddGenre(existingMetadata, genre)) { - existingMetadata.Genres.Add(genre); - if (await _movieRepository.AddGenre(existingMetadata, genre)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Studio studio in existingMetadata.Studios - .Filter(s => fullMetadata.Studios.All(s2 => s2.Name != s.Name)) - .ToList()) + foreach (Studio studio in existingMetadata.Studios + .Filter(s => fullMetadata.Studios.All(s2 => s2.Name != s.Name)) + .ToList()) + { + existingMetadata.Studios.Remove(studio); + if (await _metadataRepository.RemoveStudio(studio)) { - existingMetadata.Studios.Remove(studio); - if (await _metadataRepository.RemoveStudio(studio)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Studio studio in fullMetadata.Studios - .Filter(s => existingMetadata.Studios.All(s2 => s2.Name != s.Name)) - .ToList()) + foreach (Studio studio in fullMetadata.Studios + .Filter(s => existingMetadata.Studios.All(s2 => s2.Name != s.Name)) + .ToList()) + { + existingMetadata.Studios.Add(studio); + if (await _movieRepository.AddStudio(existingMetadata, studio)) { - existingMetadata.Studios.Add(studio); - if (await _movieRepository.AddStudio(existingMetadata, studio)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Actor actor in existingMetadata.Actors - .Filter( - a => fullMetadata.Actors.All( - a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null)) - .ToList()) + foreach (Actor actor in existingMetadata.Actors + .Filter( + a => fullMetadata.Actors.All( + a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null)) + .ToList()) + { + existingMetadata.Actors.Remove(actor); + if (await _metadataRepository.RemoveActor(actor)) { - existingMetadata.Actors.Remove(actor); - if (await _metadataRepository.RemoveActor(actor)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Actor actor in fullMetadata.Actors - .Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name)) - .ToList()) + foreach (Actor actor in fullMetadata.Actors + .Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name)) + .ToList()) + { + existingMetadata.Actors.Add(actor); + if (await _movieRepository.AddActor(existingMetadata, actor)) { - existingMetadata.Actors.Add(actor); - if (await _movieRepository.AddActor(existingMetadata, actor)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Director director in existingMetadata.Directors - .Filter(g => fullMetadata.Directors.All(g2 => g2.Name != g.Name)) - .ToList()) + foreach (Director director in existingMetadata.Directors + .Filter(g => fullMetadata.Directors.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Directors.Remove(director); + if (await _metadataRepository.RemoveDirector(director)) { - existingMetadata.Directors.Remove(director); - if (await _metadataRepository.RemoveDirector(director)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Director director in fullMetadata.Directors - .Filter(g => existingMetadata.Directors.All(g2 => g2.Name != g.Name)) - .ToList()) + foreach (Director director in fullMetadata.Directors + .Filter(g => existingMetadata.Directors.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Directors.Add(director); + if (await _movieRepository.AddDirector(existingMetadata, director)) { - existingMetadata.Directors.Add(director); - if (await _movieRepository.AddDirector(existingMetadata, director)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Writer writer in existingMetadata.Writers - .Filter(g => fullMetadata.Writers.All(g2 => g2.Name != g.Name)) - .ToList()) + foreach (Writer writer in existingMetadata.Writers + .Filter(g => fullMetadata.Writers.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Writers.Remove(writer); + if (await _metadataRepository.RemoveWriter(writer)) { - existingMetadata.Writers.Remove(writer); - if (await _metadataRepository.RemoveWriter(writer)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Writer writer in fullMetadata.Writers - .Filter(g => existingMetadata.Writers.All(g2 => g2.Name != g.Name)) - .ToList()) + foreach (Writer writer in fullMetadata.Writers + .Filter(g => existingMetadata.Writers.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Writers.Add(writer); + if (await _movieRepository.AddWriter(existingMetadata, writer)) { - existingMetadata.Writers.Add(writer); - if (await _movieRepository.AddWriter(existingMetadata, writer)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (MetadataGuid guid in existingMetadata.Guids - .Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid)) - .ToList()) + foreach (MetadataGuid guid in existingMetadata.Guids + .Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Remove(guid); + if (await _metadataRepository.RemoveGuid(guid)) { - existingMetadata.Guids.Remove(guid); - if (await _metadataRepository.RemoveGuid(guid)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (MetadataGuid guid in fullMetadata.Guids - .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) - .ToList()) + foreach (MetadataGuid guid in fullMetadata.Guids + .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Add(guid); + if (await _metadataRepository.AddGuid(existingMetadata, guid)) { - existingMetadata.Guids.Add(guid); - if (await _metadataRepository.AddGuid(existingMetadata, guid)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Tag tag in existingMetadata.Tags - .Filter(g => fullMetadata.Tags.All(g2 => g2.Name != g.Name)) - .ToList()) + foreach (Tag tag in existingMetadata.Tags + .Filter(g => fullMetadata.Tags.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Tags.Remove(tag); + if (await _metadataRepository.RemoveTag(tag)) { - existingMetadata.Tags.Remove(tag); - if (await _metadataRepository.RemoveTag(tag)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - foreach (Tag tag in fullMetadata.Tags - .Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name)) - .ToList()) + foreach (Tag tag in fullMetadata.Tags + .Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Tags.Add(tag); + if (await _movieRepository.AddTag(existingMetadata, tag)) { - existingMetadata.Tags.Add(tag); - if (await _movieRepository.AddTag(existingMetadata, tag)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - if (fullMetadata.SortTitle != existingMetadata.SortTitle) + if (fullMetadata.SortTitle != existingMetadata.SortTitle) + { + existingMetadata.SortTitle = fullMetadata.SortTitle; + if (await _movieRepository.UpdateSortTitle(existingMetadata)) { - existingMetadata.SortTitle = fullMetadata.SortTitle; - if (await _movieRepository.UpdateSortTitle(existingMetadata)) - { - result.IsUpdated = true; - } + result.IsUpdated = true; } + } - if (result.IsUpdated) - { - await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated); - } - }, - _ => Task.CompletedTask); + if (result.IsUpdated) + { + await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated); + } + } // TODO: update other metadata? diff --git a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs index 52e63c2cd..fb0ee3b93 100644 --- a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs @@ -747,83 +747,79 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL localPath); } - await refreshResult.Match( - async _ => + foreach (BaseError error in refreshResult.LeftToSeq()) + { + _logger.LogWarning( + "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", + "Statistics", + localPath, + error.Value); + } + + foreach (var _ in refreshResult.RightToSeq()) + { + foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id)) + { + await _searchIndex.UpdateItems( + _searchRepository, + new List { updated }); + } + + Either> maybeStatistics = + await _plexServerApiClient.GetEpisodeMetadataAndStatistics( + library, + incoming.Key.Split("/").Last(), + connection, + token); + + foreach (Tuple tuple in maybeStatistics.RightToSeq()) { - foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id)) + (EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple; + + Option maybeExisting = existing.EpisodeMetadata + .Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber); + foreach (EpisodeMetadata existingMetadata in maybeExisting) { - await _searchIndex.UpdateItems( - _searchRepository, - new List { updated }); - } + foreach (MetadataGuid guid in existingMetadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Remove(guid); + await _metadataRepository.RemoveGuid(guid); + } - Either> maybeStatistics = - await _plexServerApiClient.GetEpisodeMetadataAndStatistics( - library, - incoming.Key.Split("/").Last(), - connection, - token); + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Add(guid); + await _metadataRepository.AddGuid(existingMetadata, guid); + } - await maybeStatistics.Match( - async tuple => + foreach (Tag tag in existingMetadata.Tags + .Filter(g => incomingMetadata.Tags.All(g2 => g2.Name != g.Name)) + .ToList()) { - (EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple; - - Option maybeExisting = existing.EpisodeMetadata - .Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber); - foreach (EpisodeMetadata existingMetadata in maybeExisting) - { - foreach (MetadataGuid guid in existingMetadata.Guids - .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) - .ToList()) - { - existingMetadata.Guids.Remove(guid); - await _metadataRepository.RemoveGuid(guid); - } - - foreach (MetadataGuid guid in incomingMetadata.Guids - .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) - .ToList()) - { - existingMetadata.Guids.Add(guid); - await _metadataRepository.AddGuid(existingMetadata, guid); - } - - foreach (Tag tag in existingMetadata.Tags - .Filter(g => incomingMetadata.Tags.All(g2 => g2.Name != g.Name)) - .ToList()) - { - existingMetadata.Tags.Remove(tag); - await _metadataRepository.RemoveTag(tag); - } - - foreach (Tag tag in incomingMetadata.Tags - .Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name)) - .ToList()) - { - existingMetadata.Tags.Add(tag); - await _televisionRepository.AddTag(existingMetadata, tag); - } - } - - existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio; - existingVersion.VideoScanKind = mediaVersion.VideoScanKind; - existingVersion.DateUpdated = mediaVersion.DateUpdated; - - await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, mediaVersion); - }, - _ => Task.CompletedTask); - }, - error => - { - _logger.LogWarning( - "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", - "Statistics", - localPath, - error.Value); + existingMetadata.Tags.Remove(tag); + await _metadataRepository.RemoveTag(tag); + } + + foreach (Tag tag in incomingMetadata.Tags + .Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Tags.Add(tag); + await _televisionRepository.AddTag(existingMetadata, tag); + } + } + + existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio; + existingVersion.VideoScanKind = mediaVersion.VideoScanKind; + existingVersion.DateUpdated = mediaVersion.DateUpdated; - return Task.CompletedTask; - }); + await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, mediaVersion); + } + } } return result; diff --git a/docker/Dockerfile b/docker/Dockerfile index 1e6625b59..887143940 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,7 +2,7 @@ FROM jasongdove/ffmpeg:5.0-ubuntu2004 AS runtime-base COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet -RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata fontconfig fonts-dejavu +RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata fontconfig fonts-dejavu libgdiplus # https://hub.docker.com/_/microsoft-dotnet FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build diff --git a/docker/nvidia/ffmpeg.Dockerfile b/docker/nvidia/ffmpeg.Dockerfile index 805ad55db..e5b359ce6 100644 --- a/docker/nvidia/ffmpeg.Dockerfile +++ b/docker/nvidia/ffmpeg.Dockerfile @@ -3,5 +3,5 @@ FROM jasongdove/ffmpeg-base:5.0-nvidia2004 AS runtime-base COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet RUN apt-get update \ - && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata fontconfig fonts-dejavu \ + && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata fontconfig fonts-dejavu libgdiplus \ && rm -rf /var/lib/apt/lists/* diff --git a/docker/vaapi/ffmpeg.Dockerfile b/docker/vaapi/ffmpeg.Dockerfile index e39c57a14..f01d9d7eb 100644 --- a/docker/vaapi/ffmpeg.Dockerfile +++ b/docker/vaapi/ffmpeg.Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu tzdata \ fontconfig \ fonts-dejavu \ + libgdiplus \ autoconf \ libtool \ libdrm-dev \