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