Browse Source

bug fixes (#757)

* fix docker blur hash generation

* scanner async cleanup

* catch and log some unauthorized exception errors
pull/758/head
Jason Dove 4 years ago committed by GitHub
parent
commit
d879ce0d0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 191
      ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs
  3. 12
      ErsatzTV.Core/Metadata/LocalFileSystem.cs
  4. 96
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  5. 212
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  6. 316
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  7. 138
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  8. 2
      docker/Dockerfile
  9. 2
      docker/nvidia/ffmpeg.Dockerfile
  10. 1
      docker/vaapi/ffmpeg.Dockerfile

1
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 - 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 bug introduced with 0.5.2-beta that prevented some Plex content from being played
- Fix spammy subtitle error message - Fix spammy subtitle error message
- Fix generating blur hashes for song backgrounds in Docker
### Changed ### Changed
- No longer remove Plex movies and episodes from ErsatzTV when they do not exist on disk - No longer remove Plex movies and episodes from ErsatzTV when they do not exist on disk

191
ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -70,43 +70,41 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
apiKey, apiKey,
library.ItemId); library.ItemId);
await maybeShows.Match( foreach (BaseError error in maybeShows.LeftToSeq())
async shows => {
{ _logger.LogWarning(
await ProcessShows( "Error synchronizing emby library {Path}: {Error}",
address, library.Name,
apiKey, error.Value);
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<int> missingShowIds = await _televisionRepository.RemoveMissingShows(library, showIds);
await _searchIndex.RemoveItems(missingShowIds);
await _televisionRepository.DeleteEmptySeasons(library);
List<int> 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);
return Task.CompletedTask; foreach (List<EmbyShow> 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<int> missingShowIds = await _televisionRepository.RemoveMissingShows(library, showIds);
await _searchIndex.RemoveItems(missingShowIds);
await _televisionRepository.DeleteEmptySeasons(library);
List<int> emptyShowIds = await _televisionRepository.DeleteEmptyShows(library);
await _searchIndex.RemoveItems(emptyShowIds);
await _mediator.Publish(new LibraryScanProgress(library.Id, 0));
_searchIndex.Commit();
}
return Unit.Default; return Unit.Default;
} }
@ -128,39 +126,35 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion)); await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion));
Option<EmbyItemEtag> maybeExisting = existingShows.Find(ie => ie.ItemId == incoming.ItemId); Option<EmbyItemEtag> maybeExisting = existingShows.Find(ie => ie.ItemId == incoming.ItemId);
await maybeExisting.Match( if (maybeExisting.IsNone)
async existing => {
{ incoming.LibraryPathId = library.Paths.Head().Id;
if (existing.Etag == incoming.Etag)
{
return;
}
_logger.LogDebug( // _logger.LogDebug("INSERT: Item id is new for show {Show}", incoming.ShowMetadata.Head().Title);
"UPDATE: Etag has changed for show {Show}",
incoming.ShowMetadata.Head().Title);
incoming.LibraryPathId = library.Paths.Head().Id; if (await _televisionRepository.AddShow(incoming))
{
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { incoming });
}
}
Option<EmbyShow> updated = await _televisionRepository.Update(incoming); foreach (EmbyItemEtag existing in maybeExisting)
if (updated.IsSome) {
{ if (existing.Etag == incoming.Etag)
await _searchIndex.UpdateItems(
_searchRepository,
new List<MediaItem> { updated.ValueUnsafe() });
}
},
async () =>
{ {
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)) incoming.LibraryPathId = library.Paths.Head().Id;
{
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { incoming }); Option<EmbyShow> updated = await _televisionRepository.Update(incoming);
} if (updated.IsSome)
}); {
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated.ValueUnsafe() });
}
}
List<EmbyItemEtag> existingSeasons = List<EmbyItemEtag> existingSeasons =
await _televisionRepository.GetExistingSeasons(library, incoming.ItemId); await _televisionRepository.GetExistingSeasons(library, incoming.ItemId);
@ -168,36 +162,34 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
Either<BaseError, List<EmbySeason>> maybeSeasons = Either<BaseError, List<EmbySeason>> maybeSeasons =
await _embyApiClient.GetSeasonLibraryItems(address, apiKey, incoming.ItemId); await _embyApiClient.GetSeasonLibraryItems(address, apiKey, incoming.ItemId);
await maybeSeasons.Match( foreach (BaseError error in maybeSeasons.LeftToSeq())
async seasons => {
{ _logger.LogWarning(
await ProcessSeasons( "Error synchronizing emby library {Path}: {Error}",
address, library.Name,
apiKey, error.Value);
library, }
ffmpegPath,
ffprobePath,
pathReplacements,
incoming,
existingSeasons,
seasons);
var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList(); foreach (List<EmbySeason> seasons in maybeSeasons.RightToSeq())
var seasonIds = existingSeasons {
.Filter(i => !incomingSeasonIds.Contains(i.ItemId)) await ProcessSeasons(
.Map(m => m.ItemId) address,
.ToList(); apiKey,
await _televisionRepository.RemoveMissingSeasons(library, seasonIds); library,
}, ffmpegPath,
error => ffprobePath,
{ pathReplacements,
_logger.LogWarning( incoming,
"Error synchronizing emby library {Path}: {Error}", existingSeasons,
library.Name, seasons);
error.Value);
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)) foreach (MediaItem toIndex in await _searchRepository.GetItemToIndex(updated.Id))
{ {
await _searchIndex.UpdateItems( await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { toIndex });
_searchRepository,
new List<MediaItem> { toIndex });
} }
} }
}, },
@ -361,9 +351,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
Option<EmbyEpisode> maybeUpdated = await _televisionRepository.Update(incoming); Option<EmbyEpisode> maybeUpdated = await _televisionRepository.Update(incoming);
foreach (EmbyEpisode updated in maybeUpdated) foreach (EmbyEpisode updated in maybeUpdated)
{ {
await _searchIndex.UpdateItems( await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated });
_searchRepository,
new List<MediaItem> { updated });
incomingEpisode = updated; incomingEpisode = updated;
} }
@ -425,13 +413,14 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
refreshResult = await UpdateSubtitles(incomingEpisode, localPath); refreshResult = await UpdateSubtitles(incomingEpisode, localPath);
} }
refreshResult.Match( foreach (BaseError error in refreshResult.LeftToSeq())
_ => { }, {
error => _logger.LogWarning( _logger.LogWarning(
"Unable to refresh {Attribute} for media item {Path}. Error: {Error}", "Unable to refresh {Attribute} for media item {Path}. Error: {Error}",
"Statistics", "Statistics",
localPath, localPath,
error.Value)); error.Value);
}
} }
} }
} }

12
ErsatzTV.Core/Metadata/LocalFileSystem.cs

@ -47,6 +47,10 @@ public class LocalFileSystem : ILocalFileSystem
{ {
return Directory.EnumerateDirectories(folder); return Directory.EnumerateDirectories(folder);
} }
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Unauthorized access exception listing subdirectories of folder {Folder}", folder);
}
catch (Exception ex) catch (Exception ex)
{ {
// do nothing // do nothing
@ -65,6 +69,10 @@ public class LocalFileSystem : ILocalFileSystem
{ {
return Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly); return Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly);
} }
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Unauthorized access exception listing files in folder {Folder}", folder);
}
catch (Exception ex) catch (Exception ex)
{ {
// do nothing // do nothing
@ -83,6 +91,10 @@ public class LocalFileSystem : ILocalFileSystem
{ {
return Directory.EnumerateFiles(folder, searchPattern, SearchOption.TopDirectoryOnly); return Directory.EnumerateFiles(folder, searchPattern, SearchOption.TopDirectoryOnly);
} }
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Unauthorized access exception listing files in folder {Folder}", folder);
}
catch (Exception ex) catch (Exception ex)
{ {
// do nothing // do nothing

96
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -88,7 +88,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
{ {
decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count);
await _mediator.Publish( await _mediator.Publish(
new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread)); new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread),
cancellationToken);
string movieFolder = folderQueue.Dequeue(); string movieFolder = folderQueue.Dequeue();
foldersCompleted++; foldersCompleted++;
@ -142,25 +143,24 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
.BindT(UpdateSubtitles) .BindT(UpdateSubtitles)
.BindT(FlagNormal); .BindT(FlagNormal);
await maybeMovie.Match( foreach (BaseError error in maybeMovie.LeftToSeq())
async result => {
_logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value);
}
foreach (MediaItemScanResult<Movie> result in maybeMovie.RightToSeq())
{
if (result.IsAdded)
{ {
if (result.IsAdded) await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { result.Item });
{ }
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { result.Item }); else if (result.IsUpdated)
}
else if (result.IsUpdated)
{
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { result.Item });
}
await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag);
},
error =>
{ {
_logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value); await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { result.Item });
return Task.CompletedTask; }
});
await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag);
}
} }
} }
@ -192,35 +192,37 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
try try
{ {
Movie movie = result.Item; Movie movie = result.Item;
await LocateNfoFile(movie).Match(
async nfoFile => Option<string> maybeNfoFile = LocateNfoFile(movie);
if (maybeNfoFile.IsNone)
{
if (!Optional(movie.MovieMetadata).Flatten().Any())
{ {
bool shouldUpdate = Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match( string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
m => m.MetadataKind == MetadataKind.Fallback || _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile), if (await _localMetadataProvider.RefreshFallbackMetadata(movie))
true);
if (shouldUpdate)
{ {
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); result.IsUpdated = true;
if (await _localMetadataProvider.RefreshSidecarMetadata(movie, nfoFile))
{
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; result.IsUpdated = true;
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
if (await _localMetadataProvider.RefreshFallbackMetadata(movie))
{
result.IsUpdated = true;
}
} }
}); }
}
return result; return result;
} }
@ -239,12 +241,12 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
try try
{ {
Movie movie = result.Item; Movie movie = result.Item;
await LocateArtwork(movie, artworkKind).IfSomeAsync( Option<string> maybeArtwork = LocateArtwork(movie, artworkKind);
async posterFile => foreach (string posterFile in maybeArtwork)
{ {
MovieMetadata metadata = movie.MovieMetadata.Head(); MovieMetadata metadata = movie.MovieMetadata.Head();
await RefreshArtwork(posterFile, metadata, artworkKind, None, None, cancellationToken); await RefreshArtwork(posterFile, metadata, artworkKind, None, None, cancellationToken);
}); }
return result; return result;
} }

212
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -83,7 +83,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{ {
decimal percentCompletion = (decimal)allShowFolders.IndexOf(showFolder) / allShowFolders.Count; decimal percentCompletion = (decimal)allShowFolders.IndexOf(showFolder) / allShowFolders.Count;
await _mediator.Publish( await _mediator.Publish(
new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread)); new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread),
cancellationToken);
Either<BaseError, MediaItemScanResult<Show>> maybeShow = Either<BaseError, MediaItemScanResult<Show>> maybeShow =
await FindOrCreateShow(libraryPath.Id, showFolder) 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.FanArt, cancellationToken))
.BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Thumbnail, cancellationToken)); .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Thumbnail, cancellationToken));
await maybeShow.Match( foreach (BaseError error in maybeShow.LeftToSeq())
async result => {
{ _logger.LogWarning(
await ScanSeasons( "Error processing show in folder {Folder}: {Error}",
libraryPath, showFolder,
ffmpegPath, error.Value);
ffprobePath, }
result.Item,
showFolder,
cancellationToken);
if (result.IsAdded) foreach (MediaItemScanResult<Show> result in maybeShow.RightToSeq())
{ {
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { result.Item }); await ScanSeasons(libraryPath, ffmpegPath, ffprobePath, result.Item, showFolder, cancellationToken);
}
else if (result.IsUpdated) if (result.IsAdded)
{
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { result.Item });
}
},
error =>
{ {
_logger.LogWarning( await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { result.Item });
"Error processing show in folder {Folder}: {Error}", }
showFolder, else if (result.IsUpdated)
error.Value); {
return Task.FromResult(Unit.Default); await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { result.Item });
}); }
}
} }
foreach (string path in await _televisionRepository.FindEpisodePaths(libraryPath)) foreach (string path in await _televisionRepository.FindEpisodePaths(libraryPath))
@ -249,16 +243,15 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
.BindT(e => FlagNormal(new MediaItemScanResult<Episode>(e))) .BindT(e => FlagNormal(new MediaItemScanResult<Episode>(e)))
.MapT(r => r.Item); .MapT(r => r.Item);
await maybeEpisode.Match( foreach (BaseError error in maybeEpisode.LeftToSeq())
async episode => {
{ _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value);
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { episode }); }
},
error => foreach (Episode episode in maybeEpisode.RightToSeq())
{ {
_logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value); await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { episode });
return Task.CompletedTask; }
});
} }
// TODO: remove missing episodes? // TODO: remove missing episodes?
@ -273,34 +266,36 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
try try
{ {
Show show = result.Item; Show show = result.Item;
await LocateNfoFileForShow(showFolder).Match(
async nfoFile => Option<string> maybeNfo = LocateNfoFileForShow(showFolder);
if (maybeNfo.IsNone)
{
if (!Optional(show.ShowMetadata).Flatten().Any())
{ {
bool shouldUpdate = Optional(show.ShowMetadata).Flatten().HeadOrNone().Match( _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", showFolder);
m => m.MetadataKind == MetadataKind.Fallback || if (await _localMetadataProvider.RefreshFallbackMetadata(show, showFolder))
m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile),
true);
if (shouldUpdate)
{ {
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); result.IsUpdated = true;
if (await _localMetadataProvider.RefreshSidecarMetadata(show, nfoFile))
{
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); result.IsUpdated = true;
if (await _localMetadataProvider.RefreshFallbackMetadata(show, showFolder))
{
result.IsUpdated = true;
}
} }
}); }
}
return result; return result;
} }
@ -337,33 +332,34 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{ {
try try
{ {
await LocateNfoFile(episode).Match( Option<string> maybeNfo = LocateNfoFile(episode);
async nfoFile => if (maybeNfo.IsNone)
{ {
bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match( bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match(
m => m.MetadataKind == MetadataKind.Fallback || m => m.DateUpdated == SystemTime.MinValueUtc,
m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile), true);
true);
if (shouldUpdate) if (shouldUpdate)
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile);
await _localMetadataProvider.RefreshSidecarMetadata(episode, nfoFile);
}
},
async () =>
{ {
bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match( string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
m => m.DateUpdated == SystemTime.MinValueUtc, _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
true); await _localMetadataProvider.RefreshFallbackMetadata(episode);
}
}
if (shouldUpdate) foreach (string nfoFile in maybeNfo)
{ {
string path = episode.MediaVersions.Head().MediaFiles.Head().Path; bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match(
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); m => m.MetadataKind == MetadataKind.Fallback ||
await _localMetadataProvider.RefreshFallbackMetadata(episode); m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile),
} true);
});
if (shouldUpdate)
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile);
await _localMetadataProvider.RefreshSidecarMetadata(episode, nfoFile);
}
}
return episode; return episode;
} }
@ -383,12 +379,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
try try
{ {
Show show = result.Item; Show show = result.Item;
await LocateArtworkForShow(showFolder, artworkKind).IfSomeAsync( Option<string> maybeArtwork = LocateArtworkForShow(showFolder, artworkKind);
async artworkFile => foreach (string artworkFile in maybeArtwork)
{ {
ShowMetadata metadata = show.ShowMetadata.Head(); ShowMetadata metadata = show.ShowMetadata.Head();
await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken);
}); }
return result; return result;
} }
@ -406,12 +402,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{ {
try try
{ {
await LocatePoster(season, seasonFolder).IfSomeAsync( Option<string> maybePoster = LocatePoster(season, seasonFolder);
async posterFile => foreach (string posterFile in maybePoster)
{ {
SeasonMetadata metadata = season.SeasonMetadata.Head(); SeasonMetadata metadata = season.SeasonMetadata.Head();
await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster, None, None, cancellationToken); await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster, None, None, cancellationToken);
}); }
return season; return season;
} }
@ -426,20 +422,20 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{ {
try try
{ {
await LocateThumbnail(episode).IfSomeAsync( Option<string> maybeThumbnail = LocateThumbnail(episode);
async posterFile => foreach (string thumbnailFile in maybeThumbnail)
{
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
{ {
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata) await RefreshArtwork(
{ thumbnailFile,
await RefreshArtwork( metadata,
posterFile, ArtworkKind.Thumbnail,
metadata, None,
ArtworkKind.Thumbnail, None,
None, cancellationToken);
None, }
cancellationToken); }
}
});
return episode; return episode;
} }

316
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -290,28 +290,26 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
Either<BaseError, bool> refreshResult = Either<BaseError, bool> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, existing, localPath); await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, existing, localPath);
await refreshResult.Match( foreach (BaseError error in refreshResult.LeftToSeq())
async _ => {
{ _logger.LogWarning(
foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id)) "Unable to refresh {Attribute} for media item {Path}. Error: {Error}",
{ "Statistics",
await _searchIndex.UpdateItems( localPath,
_searchRepository, error.Value);
new List<MediaItem> { updated }); }
}
foreach (bool _ in refreshResult.RightToSeq())
await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, incomingVersion); {
}, foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id))
error =>
{ {
_logger.LogWarning( await _searchIndex.UpdateItems(
"Unable to refresh {Attribute} for media item {Path}. Error: {Error}", _searchRepository,
"Statistics", new List<MediaItem> { updated });
localPath, }
error.Value);
await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, incomingVersion);
return Task.CompletedTask; }
});
} }
} }
@ -340,193 +338,191 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
connection, connection,
token); token);
await maybeMetadata.Match( foreach (MovieMetadata fullMetadata in maybeMetadata.RightToSeq())
async fullMetadata => {
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) if (existingMetadata.ContentRating != fullMetadata.ContentRating)
{ {
existingMetadata.ContentRating = fullMetadata.ContentRating; existingMetadata.ContentRating = fullMetadata.ContentRating;
await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating); await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating);
result.IsUpdated = true; result.IsUpdated = true;
} }
foreach (Genre genre in existingMetadata.Genres foreach (Genre genre in existingMetadata.Genres
.Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name)) .Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name))
.ToList()) .ToList())
{
existingMetadata.Genres.Remove(genre);
if (await _metadataRepository.RemoveGenre(genre))
{ {
existingMetadata.Genres.Remove(genre); result.IsUpdated = true;
if (await _metadataRepository.RemoveGenre(genre))
{
result.IsUpdated = true;
}
} }
}
foreach (Genre genre in fullMetadata.Genres foreach (Genre genre in fullMetadata.Genres
.Filter(g => existingMetadata.Genres.All(g2 => g2.Name != g.Name)) .Filter(g => existingMetadata.Genres.All(g2 => g2.Name != g.Name))
.ToList()) .ToList())
{
existingMetadata.Genres.Add(genre);
if (await _movieRepository.AddGenre(existingMetadata, genre))
{ {
existingMetadata.Genres.Add(genre); result.IsUpdated = true;
if (await _movieRepository.AddGenre(existingMetadata, genre))
{
result.IsUpdated = true;
}
} }
}
foreach (Studio studio in existingMetadata.Studios foreach (Studio studio in existingMetadata.Studios
.Filter(s => fullMetadata.Studios.All(s2 => s2.Name != s.Name)) .Filter(s => fullMetadata.Studios.All(s2 => s2.Name != s.Name))
.ToList()) .ToList())
{
existingMetadata.Studios.Remove(studio);
if (await _metadataRepository.RemoveStudio(studio))
{ {
existingMetadata.Studios.Remove(studio); result.IsUpdated = true;
if (await _metadataRepository.RemoveStudio(studio))
{
result.IsUpdated = true;
}
} }
}
foreach (Studio studio in fullMetadata.Studios foreach (Studio studio in fullMetadata.Studios
.Filter(s => existingMetadata.Studios.All(s2 => s2.Name != s.Name)) .Filter(s => existingMetadata.Studios.All(s2 => s2.Name != s.Name))
.ToList()) .ToList())
{
existingMetadata.Studios.Add(studio);
if (await _movieRepository.AddStudio(existingMetadata, studio))
{ {
existingMetadata.Studios.Add(studio); result.IsUpdated = true;
if (await _movieRepository.AddStudio(existingMetadata, studio))
{
result.IsUpdated = true;
}
} }
}
foreach (Actor actor in existingMetadata.Actors foreach (Actor actor in existingMetadata.Actors
.Filter( .Filter(
a => fullMetadata.Actors.All( a => fullMetadata.Actors.All(
a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null)) a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null))
.ToList()) .ToList())
{
existingMetadata.Actors.Remove(actor);
if (await _metadataRepository.RemoveActor(actor))
{ {
existingMetadata.Actors.Remove(actor); result.IsUpdated = true;
if (await _metadataRepository.RemoveActor(actor))
{
result.IsUpdated = true;
}
} }
}
foreach (Actor actor in fullMetadata.Actors foreach (Actor actor in fullMetadata.Actors
.Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name)) .Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name))
.ToList()) .ToList())
{
existingMetadata.Actors.Add(actor);
if (await _movieRepository.AddActor(existingMetadata, actor))
{ {
existingMetadata.Actors.Add(actor); result.IsUpdated = true;
if (await _movieRepository.AddActor(existingMetadata, actor))
{
result.IsUpdated = true;
}
} }
}
foreach (Director director in existingMetadata.Directors foreach (Director director in existingMetadata.Directors
.Filter(g => fullMetadata.Directors.All(g2 => g2.Name != g.Name)) .Filter(g => fullMetadata.Directors.All(g2 => g2.Name != g.Name))
.ToList()) .ToList())
{
existingMetadata.Directors.Remove(director);
if (await _metadataRepository.RemoveDirector(director))
{ {
existingMetadata.Directors.Remove(director); result.IsUpdated = true;
if (await _metadataRepository.RemoveDirector(director))
{
result.IsUpdated = true;
}
} }
}
foreach (Director director in fullMetadata.Directors foreach (Director director in fullMetadata.Directors
.Filter(g => existingMetadata.Directors.All(g2 => g2.Name != g.Name)) .Filter(g => existingMetadata.Directors.All(g2 => g2.Name != g.Name))
.ToList()) .ToList())
{
existingMetadata.Directors.Add(director);
if (await _movieRepository.AddDirector(existingMetadata, director))
{ {
existingMetadata.Directors.Add(director); result.IsUpdated = true;
if (await _movieRepository.AddDirector(existingMetadata, director))
{
result.IsUpdated = true;
}
} }
}
foreach (Writer writer in existingMetadata.Writers foreach (Writer writer in existingMetadata.Writers
.Filter(g => fullMetadata.Writers.All(g2 => g2.Name != g.Name)) .Filter(g => fullMetadata.Writers.All(g2 => g2.Name != g.Name))
.ToList()) .ToList())
{
existingMetadata.Writers.Remove(writer);
if (await _metadataRepository.RemoveWriter(writer))
{ {
existingMetadata.Writers.Remove(writer); result.IsUpdated = true;
if (await _metadataRepository.RemoveWriter(writer))
{
result.IsUpdated = true;
}
} }
}
foreach (Writer writer in fullMetadata.Writers foreach (Writer writer in fullMetadata.Writers
.Filter(g => existingMetadata.Writers.All(g2 => g2.Name != g.Name)) .Filter(g => existingMetadata.Writers.All(g2 => g2.Name != g.Name))
.ToList()) .ToList())
{
existingMetadata.Writers.Add(writer);
if (await _movieRepository.AddWriter(existingMetadata, writer))
{ {
existingMetadata.Writers.Add(writer); result.IsUpdated = true;
if (await _movieRepository.AddWriter(existingMetadata, writer))
{
result.IsUpdated = true;
}
} }
}
foreach (MetadataGuid guid in existingMetadata.Guids foreach (MetadataGuid guid in existingMetadata.Guids
.Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid)) .Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList()) .ToList())
{
existingMetadata.Guids.Remove(guid);
if (await _metadataRepository.RemoveGuid(guid))
{ {
existingMetadata.Guids.Remove(guid); result.IsUpdated = true;
if (await _metadataRepository.RemoveGuid(guid))
{
result.IsUpdated = true;
}
} }
}
foreach (MetadataGuid guid in fullMetadata.Guids foreach (MetadataGuid guid in fullMetadata.Guids
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList()) .ToList())
{
existingMetadata.Guids.Add(guid);
if (await _metadataRepository.AddGuid(existingMetadata, guid))
{ {
existingMetadata.Guids.Add(guid); result.IsUpdated = true;
if (await _metadataRepository.AddGuid(existingMetadata, guid))
{
result.IsUpdated = true;
}
} }
}
foreach (Tag tag in existingMetadata.Tags foreach (Tag tag in existingMetadata.Tags
.Filter(g => fullMetadata.Tags.All(g2 => g2.Name != g.Name)) .Filter(g => fullMetadata.Tags.All(g2 => g2.Name != g.Name))
.ToList()) .ToList())
{
existingMetadata.Tags.Remove(tag);
if (await _metadataRepository.RemoveTag(tag))
{ {
existingMetadata.Tags.Remove(tag); result.IsUpdated = true;
if (await _metadataRepository.RemoveTag(tag))
{
result.IsUpdated = true;
}
} }
}
foreach (Tag tag in fullMetadata.Tags foreach (Tag tag in fullMetadata.Tags
.Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name)) .Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name))
.ToList()) .ToList())
{
existingMetadata.Tags.Add(tag);
if (await _movieRepository.AddTag(existingMetadata, tag))
{ {
existingMetadata.Tags.Add(tag); result.IsUpdated = true;
if (await _movieRepository.AddTag(existingMetadata, tag))
{
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; result.IsUpdated = true;
if (await _movieRepository.UpdateSortTitle(existingMetadata))
{
result.IsUpdated = true;
}
} }
}
if (result.IsUpdated) if (result.IsUpdated)
{ {
await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated); await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated);
} }
}, }
_ => Task.CompletedTask);
// TODO: update other metadata? // TODO: update other metadata?

138
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -747,83 +747,79 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
localPath); localPath);
} }
await refreshResult.Match( foreach (BaseError error in refreshResult.LeftToSeq())
async _ => {
_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<MediaItem> { updated });
}
Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>> maybeStatistics =
await _plexServerApiClient.GetEpisodeMetadataAndStatistics(
library,
incoming.Key.Split("/").Last(),
connection,
token);
foreach (Tuple<EpisodeMetadata, MediaVersion> tuple in maybeStatistics.RightToSeq())
{ {
foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id)) (EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple;
Option<EpisodeMetadata> maybeExisting = existing.EpisodeMetadata
.Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber);
foreach (EpisodeMetadata existingMetadata in maybeExisting)
{ {
await _searchIndex.UpdateItems( foreach (MetadataGuid guid in existingMetadata.Guids
_searchRepository, .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
new List<MediaItem> { updated }); .ToList())
} {
existingMetadata.Guids.Remove(guid);
await _metadataRepository.RemoveGuid(guid);
}
Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>> maybeStatistics = foreach (MetadataGuid guid in incomingMetadata.Guids
await _plexServerApiClient.GetEpisodeMetadataAndStatistics( .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
library, .ToList())
incoming.Key.Split("/").Last(), {
connection, existingMetadata.Guids.Add(guid);
token); await _metadataRepository.AddGuid(existingMetadata, guid);
}
await maybeStatistics.Match( foreach (Tag tag in existingMetadata.Tags
async tuple => .Filter(g => incomingMetadata.Tags.All(g2 => g2.Name != g.Name))
.ToList())
{ {
(EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple; existingMetadata.Tags.Remove(tag);
await _metadataRepository.RemoveTag(tag);
Option<EpisodeMetadata> maybeExisting = existing.EpisodeMetadata }
.Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber);
foreach (EpisodeMetadata existingMetadata in maybeExisting) foreach (Tag tag in incomingMetadata.Tags
{ .Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name))
foreach (MetadataGuid guid in existingMetadata.Guids .ToList())
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) {
.ToList()) existingMetadata.Tags.Add(tag);
{ await _televisionRepository.AddTag(existingMetadata, tag);
existingMetadata.Guids.Remove(guid); }
await _metadataRepository.RemoveGuid(guid); }
}
existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio;
foreach (MetadataGuid guid in incomingMetadata.Guids existingVersion.VideoScanKind = mediaVersion.VideoScanKind;
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) existingVersion.DateUpdated = mediaVersion.DateUpdated;
.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);
return Task.CompletedTask; await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, mediaVersion);
}); }
}
} }
return result; return result;

2
docker/Dockerfile

@ -2,7 +2,7 @@
FROM jasongdove/ffmpeg:5.0-ubuntu2004 AS runtime-base FROM jasongdove/ffmpeg:5.0-ubuntu2004 AS runtime-base
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet 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 # https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build

2
docker/nvidia/ffmpeg.Dockerfile

@ -3,5 +3,5 @@
FROM jasongdove/ffmpeg-base:5.0-nvidia2004 AS runtime-base FROM jasongdove/ffmpeg-base:5.0-nvidia2004 AS runtime-base
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
RUN apt-get update \ 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/* && rm -rf /var/lib/apt/lists/*

1
docker/vaapi/ffmpeg.Dockerfile

@ -6,6 +6,7 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu
tzdata \ tzdata \
fontconfig \ fontconfig \
fonts-dejavu \ fonts-dejavu \
libgdiplus \
autoconf \ autoconf \
libtool \ libtool \
libdrm-dev \ libdrm-dev \

Loading…
Cancel
Save