Example #1
0
    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();
        }
    }
Example #2
0
    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);
    }
Example #4
0
    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);
    }
Example #7
0
    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();
        }
    }
Example #9
0
    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();
    }
Example #10
0
    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);
    }
Example #11
0
    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));
    }
Example #12
0
    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();
        }
    }
Example #14
0
    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();
        }
    }
Example #15
0
    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));
        }
    }