public async Task <Either <BaseError, Unit> > ScanLibrary( PlexConnection connection, PlexServerAuthToken token, PlexLibrary plexMediaSourceLibrary) { Either <BaseError, List <PlexShow> > entries = await _plexServerApiClient.GetShowLibraryContents( plexMediaSourceLibrary, connection, token); return(await entries.Match <Task <Either <BaseError, Unit> > >( async showEntries => { foreach (PlexShow incoming in showEntries) { // TODO: figure out how to rebuild playlists Either <BaseError, MediaItemScanResult <PlexShow> > maybeShow = await _televisionRepository .GetOrAddPlexShow(plexMediaSourceLibrary, incoming) .BindT(existing => UpdateMetadata(existing, incoming)) .BindT(existing => UpdateArtwork(existing, incoming)); await maybeShow.Match( async result => { if (result.IsAdded) { await _searchIndex.AddItems(new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(new List <MediaItem> { result.Item }); } await ScanSeasons(plexMediaSourceLibrary, result.Item, connection, token); }, error => { _logger.LogWarning( "Error processing plex show at {Key}: {Error}", incoming.Key, error.Value); return Task.CompletedTask; }); } var showKeys = showEntries.Map(s => s.Key).ToList(); List <int> ids = await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys); await _searchIndex.RemoveItems(ids); return Unit.Default; }, error => { _logger.LogWarning( "Error synchronizing plex library {Path}: {Error}", plexMediaSourceLibrary.Name, error.Value); return Left <BaseError, Unit>(error).AsTask(); })); }
public async Task <Either <BaseError, Unit> > ScanFolder( LibraryPath libraryPath, string ffmpegPath, string ffprobePath, decimal progressMin, decimal progressMax, CancellationToken cancellationToken) { try { decimal progressSpread = progressMax - progressMin; var allArtistFolders = _localFileSystem.ListSubdirectories(libraryPath.Path) .Filter(ShouldIncludeFolder) .OrderBy(identity) .ToList(); foreach (string artistFolder in allArtistFolders) { // _logger.LogDebug("Scanning artist folder {Folder}", artistFolder); if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } decimal percentCompletion = (decimal)allArtistFolders.IndexOf(artistFolder) / allArtistFolders.Count; await _mediator.Publish( new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion *progressSpread), cancellationToken); Either <BaseError, MediaItemScanResult <Artist> > maybeArtist = await FindOrCreateArtist(libraryPath.Id, artistFolder) .BindT(artist => UpdateMetadataForArtist(artist, artistFolder)) .BindT( artist => UpdateArtworkForArtist( artist, artistFolder, ArtworkKind.Thumbnail, cancellationToken)) .BindT( artist => UpdateArtworkForArtist( artist, artistFolder, ArtworkKind.FanArt, cancellationToken)); foreach (BaseError error in maybeArtist.LeftToSeq()) { _logger.LogWarning( "Error processing artist in folder {Folder}: {Error}", artistFolder, error.Value); } foreach (MediaItemScanResult <Artist> result in maybeArtist.RightToSeq()) { Either <BaseError, Unit> scanResult = await ScanMusicVideos( libraryPath, ffmpegPath, ffprobePath, result.Item, artistFolder, cancellationToken); foreach (ScanCanceled error in scanResult.LeftToSeq().OfType <ScanCanceled>()) { return(error); } if (result.IsAdded) { await _searchIndex.AddItems(_searchRepository, new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(_searchRepository, new List <MediaItem> { result.Item }); } } } foreach (string path in await _musicVideoRepository.FindOrphanPaths(libraryPath)) { _logger.LogInformation("Removing improperly named music video at {Path}", path); List <int> musicVideoIds = await _musicVideoRepository.DeleteByPath(libraryPath, path); await _searchIndex.RemoveItems(musicVideoIds); } foreach (string path in await _musicVideoRepository.FindMusicVideoPaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Flagging missing music video at {Path}", path); List <int> musicVideoIds = await FlagFileNotFound(libraryPath, path); await _searchIndex.RebuildItems(_searchRepository, musicVideoIds); } else if (Path.GetFileName(path).StartsWith("._")) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List <int> musicVideoIds = await _musicVideoRepository.DeleteByPath(libraryPath, path); await _searchIndex.RemoveItems(musicVideoIds); } } await _libraryRepository.CleanEtagsForLibraryPath(libraryPath); List <int> artistIds = await _artistRepository.DeleteEmptyArtists(libraryPath); await _searchIndex.RemoveItems(artistIds); return(Unit.Default); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { _searchIndex.Commit(); } }
public async Task <Either <BaseError, Unit> > ScanFolder(LibraryPath libraryPath, string ffprobePath) { if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) { return(new MediaSourceInaccessible()); } var folderQueue = new Queue <string>(); foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) { folderQueue.Enqueue(folder); } while (folderQueue.Count > 0) { string movieFolder = folderQueue.Dequeue(); var allFiles = _localFileSystem.ListFiles(movieFolder) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter( f => !ExtraFiles.Any( e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase))) .ToList(); if (allFiles.Count == 0) { foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } continue; } foreach (string file in allFiles.OrderBy(identity)) { // TODO: figure out how to rebuild playlists Either <BaseError, MediaItemScanResult <Movie> > maybeMovie = await _movieRepository .GetOrAdd(libraryPath, file) .BindT(movie => UpdateStatistics(movie, ffprobePath)) .BindT(UpdateMetadata) .BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster)) .BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt)); await maybeMovie.Match( async result => { if (result.IsAdded) { await _searchIndex.AddItems(new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(new List <MediaItem> { result.Item }); } }, error => { _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value); return(Task.CompletedTask); }); } } foreach (string path in await _movieRepository.FindMoviePaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Removing missing movie at {Path}", path); List <int> ids = await _movieRepository.DeleteByPath(libraryPath, path); await _searchIndex.RemoveItems(ids); } } return(Unit.Default); }
public async Task <Either <BaseError, Unit> > ScanFolder( LibraryPath libraryPath, string ffmpegPath, string ffprobePath, decimal progressMin, decimal progressMax, CancellationToken cancellationToken) { try { decimal progressSpread = progressMax - progressMin; var foldersCompleted = 0; var folderQueue = new Queue <string>(); if (ShouldIncludeFolder(libraryPath.Path)) { folderQueue.Enqueue(libraryPath.Path); } foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path) .Filter(ShouldIncludeFolder) .OrderBy(identity)) { folderQueue.Enqueue(folder); } while (folderQueue.Count > 0) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); await _mediator.Publish( new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion *progressSpread), cancellationToken); string otherVideoFolder = folderQueue.Dequeue(); foldersCompleted++; var filesForEtag = _localFileSystem.ListFiles(otherVideoFolder).ToList(); var allFiles = filesForEtag .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => !Path.GetFileName(f).StartsWith("._")) .ToList(); foreach (string subdirectory in _localFileSystem.ListSubdirectories(otherVideoFolder) .Filter(ShouldIncludeFolder) .OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } string etag = FolderEtag.Calculate(otherVideoFolder, _localFileSystem); Option <LibraryFolder> knownFolder = libraryPath.LibraryFolders .Filter(f => f.Path == otherVideoFolder) .HeadOrNone(); // skip folder if etag matches if (!allFiles.Any() || await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag) { continue; } _logger.LogDebug( "UPDATE: Etag has changed for folder {Folder}", otherVideoFolder); foreach (string file in allFiles.OrderBy(identity)) { Either <BaseError, MediaItemScanResult <OtherVideo> > maybeVideo = await _otherVideoRepository .GetOrAdd(libraryPath, file) .BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath)) .BindT(UpdateMetadata) .BindT(UpdateSubtitles) .BindT(FlagNormal); foreach (BaseError error in maybeVideo.LeftToSeq()) { _logger.LogWarning("Error processing other video at {Path}: {Error}", file, error.Value); } foreach (MediaItemScanResult <OtherVideo> result in maybeVideo.RightToSeq()) { if (result.IsAdded) { await _searchIndex.AddItems(_searchRepository, new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(_searchRepository, new List <MediaItem> { result.Item }); } await _libraryRepository.SetEtag(libraryPath, knownFolder, otherVideoFolder, etag); } } } foreach (string path in await _otherVideoRepository.FindOtherVideoPaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Flagging missing other video at {Path}", path); List <int> otherVideoIds = await FlagFileNotFound(libraryPath, path); await _searchIndex.RebuildItems(_searchRepository, otherVideoIds); } else if (Path.GetFileName(path).StartsWith("._")) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List <int> otherVideoIds = await _otherVideoRepository.DeleteByPath(libraryPath, path); await _searchIndex.RemoveItems(otherVideoIds); } } await _libraryRepository.CleanEtagsForLibraryPath(libraryPath); return(Unit.Default); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { _searchIndex.Commit(); } }
public async Task <Either <BaseError, Unit> > ScanLibrary( string address, string apiKey, JellyfinLibrary library, string ffmpegPath, string ffprobePath, CancellationToken cancellationToken) { try { List <JellyfinItemEtag> existingShows = await _televisionRepository.GetExistingShows(library); // TODO: maybe get quick list of item ids and etags from api to compare first // TODO: paging? List <JellyfinPathReplacement> pathReplacements = await _mediaSourceRepository .GetJellyfinPathReplacements(library.MediaSourceId); Either <BaseError, List <JellyfinShow> > maybeShows = await _jellyfinApiClient.GetShowLibraryItems( address, apiKey, library.MediaSourceId, library.ItemId); foreach (BaseError error in maybeShows.LeftToSeq()) { _logger.LogWarning( "Error synchronizing jellyfin library {Path}: {Error}", library.Name, error.Value); } foreach (List <JellyfinShow> shows in maybeShows.RightToSeq()) { Either <BaseError, Unit> scanResult = await ProcessShows( address, apiKey, library, ffmpegPath, ffprobePath, pathReplacements, existingShows, shows, cancellationToken); foreach (ScanCanceled error in scanResult.LeftToSeq().OfType <ScanCanceled>()) { return(error); } foreach (Unit _ in scanResult.RightToSeq()) { 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), cancellationToken); } } return(Unit.Default); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { _searchIndex.Commit(); } }
public async Task <Either <BaseError, Unit> > ScanLibrary( PlexConnection connection, PlexServerAuthToken token, PlexLibrary plexMediaSourceLibrary) { Either <BaseError, List <PlexMovie> > entries = await _plexServerApiClient.GetMovieLibraryContents( plexMediaSourceLibrary, connection, token); await entries.Match( async movieEntries => { foreach (PlexMovie incoming in movieEntries) { // TODO: figure out how to rebuild playlists Either <BaseError, MediaItemScanResult <PlexMovie> > maybeMovie = await _movieRepository .GetOrAdd(plexMediaSourceLibrary, incoming) .BindT(existing => UpdateStatistics(existing, incoming, connection, token)) .BindT(existing => UpdateMetadata(existing, incoming)) .BindT(existing => UpdateArtwork(existing, incoming)); await maybeMovie.Match( async result => { if (result.IsAdded) { await _searchIndex.AddItems(new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(new List <MediaItem> { result.Item }); } }, error => { _logger.LogWarning( "Error processing plex movie at {Key}: {Error}", incoming.Key, error.Value); return(Task.CompletedTask); }); } var movieKeys = movieEntries.Map(s => s.Key).ToList(); List <int> ids = await _movieRepository.RemoveMissingPlexMovies(plexMediaSourceLibrary, movieKeys); await _searchIndex.RemoveItems(ids); }, error => { _logger.LogWarning( "Error synchronizing plex library {Path}: {Error}", plexMediaSourceLibrary.Name, error.Value); return(Task.CompletedTask); }); return(Unit.Default); }