From cbae355402b78ef756d6ed84588c5c6b11800d8b Mon Sep 17 00:00:00 2001 From: Ricardo Christmann <80476005+ricci2511@users.noreply.github.com> Date: Sun, 25 Jun 2023 16:04:57 +0200 Subject: [PATCH] New: Added filter and sort options to Collections (#8731) * New: Added filter and sort options to Collections * Add AllMovieWithCollectionsTmdbIds method to MovieService and MovieRepository --- .../src/Collection/CollectionItemConnector.js | 21 +++------------- .../Collection/Menus/CollectionSortMenu.js | 8 +++++++ .../Store/Actions/movieCollectionActions.js | 24 ++++++++++++++++++- src/NzbDrone.Core/Movies/MovieRepository.cs | 9 +++++++ src/NzbDrone.Core/Movies/MovieService.cs | 6 +++++ .../Collections/CollectionController.cs | 12 ++++++++++ .../Collections/CollectionResource.cs | 1 + 7 files changed, 62 insertions(+), 19 deletions(-) diff --git a/frontend/src/Collection/CollectionItemConnector.js b/frontend/src/Collection/CollectionItemConnector.js index d94fa6345..40e801810 100644 --- a/frontend/src/Collection/CollectionItemConnector.js +++ b/frontend/src/Collection/CollectionItemConnector.js @@ -2,17 +2,12 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; import createCollectionSelector from 'Store/Selectors/createCollectionSelector'; function createMapStateToProps() { return createSelector( createCollectionSelector(), - createAllMoviesSelector(), - ( - collection, - allMovies - ) => { + (collection) => { // If a movie is deleted this selector may fire before the parent // selecors, which will result in an undefined movie, if that happens // we want to return early here and again in the render function to avoid @@ -22,21 +17,11 @@ function createMapStateToProps() { return {}; } - let allGenres = []; - let libraryMovies = 0; - - collection.movies.forEach((movie) => { - allGenres = allGenres.concat(movie.genres); - - if (allMovies.find((libraryMovie) => libraryMovie.tmdbId === movie.tmdbId)) { - libraryMovies++; - } - }); + const allGenres = collection.movies.flatMap((movie) => movie.genres); return { ...collection, - genres: Array.from(new Set(allGenres)).slice(0, 3), - missingMovies: collection.movies.length - libraryMovies + genres: Array.from(new Set(allGenres)).slice(0, 3) }; } ); diff --git a/frontend/src/Collection/Menus/CollectionSortMenu.js b/frontend/src/Collection/Menus/CollectionSortMenu.js index 9738fa69f..29c8cf12a 100644 --- a/frontend/src/Collection/Menus/CollectionSortMenu.js +++ b/frontend/src/Collection/Menus/CollectionSortMenu.js @@ -28,6 +28,14 @@ function CollectionSortMenu(props) { > {translate('Title')} + + {translate('Missing')} + ); diff --git a/frontend/src/Store/Actions/movieCollectionActions.js b/frontend/src/Store/Actions/movieCollectionActions.js index 5897f44af..1ae1a2e3b 100644 --- a/frontend/src/Store/Actions/movieCollectionActions.js +++ b/frontend/src/Store/Actions/movieCollectionActions.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; -import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, sortDirections } from 'Helpers/Props'; +import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import sortByName from 'Utilities/Array/sortByName'; import createAjaxRequest from 'Utilities/createAjaxRequest'; @@ -62,6 +62,28 @@ export const defaultState = { key: 'all', label: 'All', filters: [] + }, + { + key: 'missing', + label: 'Missing', + filters: [ + { + key: 'missingMovies', + value: 0, + type: filterTypes.GREATER_THAN + } + ] + }, + { + key: 'complete', + label: 'Complete', + filters: [ + { + key: 'missingMovies', + value: 0, + type: filterTypes.EQUAL + } + ] } ], diff --git a/src/NzbDrone.Core/Movies/MovieRepository.cs b/src/NzbDrone.Core/Movies/MovieRepository.cs index 02aa448e2..247bd9e25 100644 --- a/src/NzbDrone.Core/Movies/MovieRepository.cs +++ b/src/NzbDrone.Core/Movies/MovieRepository.cs @@ -31,6 +31,7 @@ namespace NzbDrone.Core.Movies Dictionary> AllMovieTags(); List GetRecommendations(); bool ExistsByMetadataId(int metadataId); + HashSet AllMovieWithCollectionsTmdbIds(); } public class MovieRepository : BasicRepository, IMovieRepository @@ -373,5 +374,13 @@ namespace NzbDrone.Core.Movies return movies.Any(); } + + public HashSet AllMovieWithCollectionsTmdbIds() + { + using (var conn = _database.OpenConnection()) + { + return conn.Query("SELECT \"TmdbId\" FROM \"MovieMetadata\" JOIN \"Movies\" ON (\"Movies\".\"MovieMetadataId\" = \"MovieMetadata\".\"Id\") WHERE \"CollectionTmdbId\" > 0").ToHashSet(); + } + } } } diff --git a/src/NzbDrone.Core/Movies/MovieService.cs b/src/NzbDrone.Core/Movies/MovieService.cs index f0211d528..b322f265e 100644 --- a/src/NzbDrone.Core/Movies/MovieService.cs +++ b/src/NzbDrone.Core/Movies/MovieService.cs @@ -48,6 +48,7 @@ namespace NzbDrone.Core.Movies bool MoviePathExists(string folder); void RemoveAddOptions(Movie movie); bool ExistsByMetadataId(int metadataId); + HashSet AllMovieWithCollectionsTmdbIds(); } public class MovieService : IMovieService, IHandle, @@ -390,6 +391,11 @@ namespace NzbDrone.Core.Movies return _movieRepository.ExistsByMetadataId(metadataId); } + public HashSet AllMovieWithCollectionsTmdbIds() + { + return _movieRepository.AllMovieWithCollectionsTmdbIds(); + } + private Movie ReturnSingleMovieOrThrow(List movies) { if (movies.Count == 0) diff --git a/src/Radarr.Api.V3/Collections/CollectionController.cs b/src/Radarr.Api.V3/Collections/CollectionController.cs index 8e08067d4..5a40fb931 100644 --- a/src/Radarr.Api.V3/Collections/CollectionController.cs +++ b/src/Radarr.Api.V3/Collections/CollectionController.cs @@ -135,6 +135,7 @@ namespace Radarr.Api.V3.Collections // Avoid calling for naming spec on every movie in filenamebuilder var namingConfig = _namingService.GetConfig(); var collectionMovies = _movieMetadataService.GetMoviesWithCollections(); + var existingMoviesTmdbIds = _movieService.AllMovieWithCollectionsTmdbIds(); foreach (var collection in collections) { @@ -145,6 +146,11 @@ namespace Radarr.Api.V3.Collections var movieResource = movie.ToResource(); movieResource.Folder = _fileNameBuilder.GetMovieFolder(new Movie { MovieMetadata = movie }, namingConfig); + if (!existingMoviesTmdbIds.Contains(movie.TmdbId)) + { + resource.MissingMovies++; + } + resource.Movies.Add(movieResource); } @@ -155,12 +161,18 @@ namespace Radarr.Api.V3.Collections private CollectionResource MapToResource(MovieCollection collection) { var resource = collection.ToResource(); + var existingMoviesTmdbIds = _movieService.AllMovieWithCollectionsTmdbIds(); foreach (var movie in _movieMetadataService.GetMoviesByCollectionTmdbId(collection.TmdbId)) { var movieResource = movie.ToResource(); movieResource.Folder = _fileNameBuilder.GetMovieFolder(new Movie { MovieMetadata = movie }); + if (!existingMoviesTmdbIds.Contains(movie.TmdbId)) + { + resource.MissingMovies++; + } + resource.Movies.Add(movieResource); } diff --git a/src/Radarr.Api.V3/Collections/CollectionResource.cs b/src/Radarr.Api.V3/Collections/CollectionResource.cs index 88b6b1bdf..6b54e5e08 100644 --- a/src/Radarr.Api.V3/Collections/CollectionResource.cs +++ b/src/Radarr.Api.V3/Collections/CollectionResource.cs @@ -25,6 +25,7 @@ namespace Radarr.Api.V3.Collections public bool SearchOnAdd { get; set; } public MovieStatusType MinimumAvailability { get; set; } public List Movies { get; set; } + public int MissingMovies { get; set; } } public static class CollectionResourceMapper