public async Task <Either <BaseError, Unit> > ScanLibrary( PlexConnection connection, PlexServerAuthToken token, PlexLibrary library, string ffmpegPath, string ffprobePath, bool deepScan, CancellationToken cancellationToken) { try { Either <BaseError, List <PlexShow> > entries = await _plexServerApiClient.GetShowLibraryContents( library, connection, token); foreach (BaseError error in entries.LeftToSeq()) { return(error); } return(await ScanLibrary( connection, token, library, ffmpegPath, ffprobePath, deepScan, entries.RightToSeq().Flatten().ToList(), cancellationToken)); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { // always commit the search index to prevent corruption _searchIndex.Commit(); } }
public async Task <Either <BaseError, Unit> > Handle(SignOutOfPlex request, CancellationToken cancellationToken) { List <int> ids = await _mediaSourceRepository.DeleteAllPlex(); await _searchIndex.RemoveItems(ids); _searchIndex.Commit(); await _plexSecretStore.DeleteAll(); _entityLocker.UnlockPlex(); return(Unit.Default); }
public async Task <Either <BaseError, Unit> > Handle( DisconnectEmby request, CancellationToken cancellationToken) { List <int> ids = await _mediaSourceRepository.DeleteAllEmby(); await _searchIndex.RemoveItems(ids); _searchIndex.Commit(); await _embySecretStore.DeleteAll(); _entityLocker.UnlockRemoteMediaSource <EmbyMediaSource>(); return(Unit.Default); }
public async Task <Either <BaseError, Unit> > Handle( DeleteItemsFromDatabase request, CancellationToken cancellationToken) { Either <BaseError, Unit> deleteResult = await _mediaItemRepository.DeleteItems(request.MediaItemIds); if (deleteResult.IsRight) { await _searchIndex.RemoveItems(request.MediaItemIds); _searchIndex.Commit(); } return(deleteResult); }
public async Task <Either <BaseError, Unit> > Handle( UpdateJellyfinLibraryPreferences request, CancellationToken cancellationToken) { var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); List <int> ids = await _mediaSourceRepository.DisableJellyfinLibrarySync(toDisable); await _searchIndex.RemoveItems(ids); _searchIndex.Commit(); IEnumerable <int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id); await _mediaSourceRepository.EnableJellyfinLibrarySync(toEnable); return(Unit.Default); }
private async Task <Unit> DoDeletion(TvContext dbContext, LocalLibrary localLibrary) { List <int> ids = await dbContext.Connection.QueryAsync <int>( @"SELECT MediaItem.Id FROM MediaItem INNER JOIN LibraryPath LP on MediaItem.LibraryPathId = LP.Id WHERE LP.LibraryId = @LibraryId", new { LibraryId = localLibrary.Id }) .Map(result => result.ToList()); await _searchIndex.RemoveItems(ids); _searchIndex.Commit(); dbContext.LocalLibraries.Remove(localLibrary); await dbContext.SaveChangesAsync(); return(Unit.Default); }
private async Task <Unit> DoDeletion(TvContext dbContext, TraktList traktList) { var mediaItemIds = traktList.Items.Bind(i => Optional(i.MediaItemId)).ToList(); dbContext.TraktLists.Remove(traktList); if (await dbContext.SaveChangesAsync() > 0) { foreach (int mediaItemId in mediaItemIds) { foreach (MediaItem mediaItem in await _searchRepository.GetItemToIndex(mediaItemId)) { await _searchIndex.UpdateItems(_searchRepository, new[] { mediaItem }.ToList()); } } } _searchIndex.Commit(); return(Unit.Default); }
protected async Task <Either <BaseError, Unit> > ScanLibrary( IMediaServerMovieRepository <TLibrary, TMovie, TEtag> movieRepository, TConnectionParameters connectionParameters, TLibrary library, Func <TMovie, string> getLocalPath, string ffmpegPath, string ffprobePath, bool deepScan, CancellationToken cancellationToken) { try { Either <BaseError, List <TMovie> > entries = await GetMovieLibraryItems(connectionParameters, library); foreach (BaseError error in entries.LeftToSeq()) { return(error); } return(await ScanLibrary( movieRepository, connectionParameters, library, getLocalPath, ffmpegPath, ffprobePath, entries.RightToSeq().Flatten().ToList(), deepScan, cancellationToken)); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { _searchIndex.Commit(); } }
private async Task SyncCollectionItems( string address, string apiKey, int mediaSourceId, JellyfinCollection collection) { // get collection items from JF Either <BaseError, List <MediaItem> > maybeItems = await _jellyfinApiClient.GetCollectionItems(address, apiKey, mediaSourceId, collection.ItemId); foreach (BaseError error in maybeItems.LeftToSeq()) { _logger.LogWarning("Failed to get collection items from Jellyfin: {Error}", error.ToString()); return; } List <int> removedIds = await _jellyfinCollectionRepository.RemoveAllTags(collection); var jellyfinItems = maybeItems.RightToSeq().Flatten().ToList(); _logger.LogDebug("Jellyfin collection {Name} contains {Count} items", collection.Name, jellyfinItems.Count); // sync tags on items var addedIds = new List <int>(); foreach (MediaItem item in jellyfinItems) { addedIds.Add(await _jellyfinCollectionRepository.AddTag(item, collection)); } var changedIds = removedIds.Except(addedIds).ToList(); changedIds.AddRange(addedIds.Except(removedIds)); await _searchIndex.RebuildItems(_searchRepository, changedIds); _searchIndex.Commit(); }
public async Task <Either <BaseError, Unit> > Handle( EmptyTrash request, CancellationToken cancellationToken) { string[] types = { SearchIndex.MovieType, SearchIndex.ShowType, SearchIndex.SeasonType, SearchIndex.EpisodeType, SearchIndex.MusicVideoType, SearchIndex.OtherVideoType, SearchIndex.SongType, SearchIndex.ArtistType }; var ids = new List <int>(); foreach (string type in types) { SearchResult result = await _searchIndex.Search($"type:{type} AND (state:FileNotFound)", 0, 0); ids.AddRange(result.Items.Map(i => i.Id)); } Either <BaseError, Unit> deleteResult = await _mediaItemRepository.DeleteItems(ids); if (deleteResult.IsRight) { await _searchIndex.RemoveItems(ids); _searchIndex.Commit(); } return(deleteResult); }
private async Task <LocalLibraryViewModel> UpdateLocalLibrary(TvContext dbContext, Parameters parameters) { (LocalLibrary existing, LocalLibrary incoming) = parameters; existing.Name = incoming.Name; var toAdd = incoming.Paths .Filter(p => existing.Paths.All(ep => NormalizePath(ep.Path) != NormalizePath(p.Path))) .ToList(); var toRemove = existing.Paths .Filter(ep => incoming.Paths.All(p => NormalizePath(p.Path) != NormalizePath(ep.Path))) .ToList(); var toRemoveIds = toRemove.Map(lp => lp.Id).ToList(); List <int> itemsToRemove = await dbContext.MediaItems .Filter(mi => toRemoveIds.Contains(mi.LibraryPathId)) .Map(mi => mi.Id) .ToListAsync(); existing.Paths.RemoveAll(toRemove.Contains); existing.Paths.AddRange(toAdd); if (await dbContext.SaveChangesAsync() > 0) { await _searchIndex.RemoveItems(itemsToRemove); _searchIndex.Commit(); } if ((toAdd.Count > 0 || toRemove.Count > 0) && _entityLocker.LockLibrary(existing.Id)) { await _workerChannel.WriteAsync(new ForceScanLocalLibrary(existing.Id)); } return(ProjectToViewModel(existing)); }
public async Task <Either <BaseError, Unit> > ScanLibrary( string address, string apiKey, EmbyLibrary library, string ffmpegPath, string ffprobePath, CancellationToken cancellationToken) { try { List <EmbyItemEtag> existingShows = await _televisionRepository.GetExistingShows(library); // TODO: maybe get quick list of item ids and etags from api to compare first // TODO: paging? List <EmbyPathReplacement> pathReplacements = await _mediaSourceRepository .GetEmbyPathReplacements(library.MediaSourceId); Either <BaseError, List <EmbyShow> > maybeShows = await _embyApiClient.GetShowLibraryItems( address, apiKey, library.ItemId); foreach (BaseError error in maybeShows.LeftToSeq()) { _logger.LogWarning( "Error synchronizing emby library {Path}: {Error}", library.Name, error.Value); } foreach (List <EmbyShow> 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> > ScanFolder( LibraryPath libraryPath, string ffmpegPath, string ffprobePath, decimal progressMin, decimal progressMax, CancellationToken cancellationToken) { try { decimal progressSpread = progressMax - progressMin; var allShowFolders = _localFileSystem.ListSubdirectories(libraryPath.Path) .Filter(ShouldIncludeFolder) .OrderBy(identity) .ToList(); foreach (string showFolder in allShowFolders) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } decimal percentCompletion = (decimal)allShowFolders.IndexOf(showFolder) / allShowFolders.Count; await _mediator.Publish( new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion *progressSpread), cancellationToken); Either <BaseError, MediaItemScanResult <Show> > maybeShow = await FindOrCreateShow(libraryPath.Id, showFolder) .BindT(show => UpdateMetadataForShow(show, showFolder)) .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Poster, cancellationToken)) .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.FanArt, cancellationToken)) .BindT( show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Thumbnail, cancellationToken)); foreach (BaseError error in maybeShow.LeftToSeq()) { _logger.LogWarning( "Error processing show in folder {Folder}: {Error}", showFolder, error.Value); } foreach (MediaItemScanResult <Show> result in maybeShow.RightToSeq()) { Either <BaseError, Unit> scanResult = await ScanSeasons( libraryPath, ffmpegPath, ffprobePath, result.Item, showFolder, 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 _televisionRepository.FindEpisodePaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Flagging missing episode at {Path}", path); List <int> episodeIds = await FlagFileNotFound(libraryPath, path); await _searchIndex.RebuildItems(_searchRepository, episodeIds); } else if (Path.GetFileName(path).StartsWith("._")) { _logger.LogInformation("Removing dot underscore file at {Path}", path); await _televisionRepository.DeleteByPath(libraryPath, path); } } await _libraryRepository.CleanEtagsForLibraryPath(libraryPath); await _televisionRepository.DeleteEmptySeasons(libraryPath); List <int> ids = await _televisionRepository.DeleteEmptyShows(libraryPath); await _searchIndex.RemoveItems(ids); 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, string ffmpegPath, 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 songFolder = folderQueue.Dequeue(); foldersCompleted++; var filesForEtag = _localFileSystem.ListFiles(songFolder).ToList(); var allFiles = filesForEtag .Filter(f => AudioFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => !Path.GetFileName(f).StartsWith("._")) .ToList(); foreach (string subdirectory in _localFileSystem.ListSubdirectories(songFolder) .Filter(ShouldIncludeFolder) .OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } string etag = FolderEtag.Calculate(songFolder, _localFileSystem); Option <LibraryFolder> knownFolder = libraryPath.LibraryFolders .Filter(f => f.Path == songFolder) .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}", songFolder); foreach (string file in allFiles.OrderBy(identity)) { Either <BaseError, MediaItemScanResult <Song> > maybeSong = await _songRepository .GetOrAdd(libraryPath, file) .BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath)) .BindT(video => UpdateMetadata(video, ffprobePath)) .BindT(video => UpdateThumbnail(video, ffmpegPath, cancellationToken)) .BindT(FlagNormal); foreach (BaseError error in maybeSong.LeftToSeq()) { _logger.LogWarning("Error processing song at {Path}: {Error}", file, error.Value); } foreach (MediaItemScanResult <Song> result in maybeSong.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, songFolder, etag); } } } foreach (string path in await _songRepository.FindSongPaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Flagging missing song at {Path}", path); List <int> songIds = await FlagFileNotFound(libraryPath, path); await _searchIndex.RebuildItems(_searchRepository, songIds); } else if (Path.GetFileName(path).StartsWith("._")) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List <int> songIds = await _songRepository.DeleteByPath(libraryPath, path); await _searchIndex.RemoveItems(songIds); } } await _libraryRepository.CleanEtagsForLibraryPath(libraryPath); return(Unit.Default); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { _searchIndex.Commit(); } }
protected async Task <Either <BaseError, TraktList> > MatchListItems(TvContext dbContext, TraktList list) { try { var ids = new System.Collections.Generic.HashSet <int>(); foreach (TraktListItem item in list.Items .OrderBy(i => i.Title).ThenBy(i => i.Year).ThenBy(i => i.Season).ThenBy(i => i.Episode)) { switch (item.Kind) { case TraktListItemKind.Movie: Option <int> maybeMovieId = await IdentifyMovie(dbContext, item); foreach (int movieId in maybeMovieId) { ids.Add(movieId); item.MediaItemId = movieId; } break; case TraktListItemKind.Show: Option <int> maybeShowId = await IdentifyShow(dbContext, item); foreach (int showId in maybeShowId) { ids.Add(showId); item.MediaItemId = showId; } break; case TraktListItemKind.Season: Option <int> maybeSeasonId = await IdentifySeason(dbContext, item); foreach (int seasonId in maybeSeasonId) { ids.Add(seasonId); item.MediaItemId = seasonId; } break; default: Option <int> maybeEpisodeId = await IdentifyEpisode(dbContext, item); foreach (int episodeId in maybeEpisodeId) { ids.Add(episodeId); item.MediaItemId = episodeId; } break; } } await dbContext.SaveChangesAsync(); foreach (int mediaItemId in ids) { Option <MediaItem> maybeItem = await _searchRepository.GetItemToIndex(mediaItemId); foreach (MediaItem item in maybeItem) { await _searchIndex.UpdateItems(_searchRepository, new[] { item }.ToList()); } } _searchIndex.Commit(); return(list); } catch (Exception ex) { _logger.LogError(ex, "Error matching trakt list items"); return(BaseError.New(ex.Message)); } }