diff --git a/frontend/src/Commands/commandNames.js b/frontend/src/Commands/commandNames.js index ff5942136..a4ec16d41 100644 --- a/frontend/src/Commands/commandNames.js +++ b/frontend/src/Commands/commandNames.js @@ -3,12 +3,12 @@ export const BACKUP = 'Backup'; export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload'; export const CLEAR_BLACKLIST = 'ClearBlacklist'; export const CLEAR_LOGS = 'ClearLog'; -export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch'; +export const CUTOFF_UNMET_MOVIES_SEARCH = 'CutoffUnmetMoviesSearch'; export const DELETE_LOG_FILES = 'DeleteLogFiles'; export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles'; export const DOWNLOADED_EPSIODES_SCAN = 'DownloadedEpisodesScan'; export const INTERACTIVE_IMPORT = 'ManualImport'; -export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch'; +export const MISSING_MOVIES_SEARCH = 'MissingMoviesSearch'; export const MOVE_MOVIE = 'MoveMovie'; export const REFRESH_MOVIE = 'RefreshMovie'; export const RENAME_FILES = 'RenameFiles'; diff --git a/frontend/src/Components/Menu/SearchMenuItem.js b/frontend/src/Components/Menu/SearchMenuItem.js new file mode 100644 index 000000000..e9a75c9b2 --- /dev/null +++ b/frontend/src/Components/Menu/SearchMenuItem.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MenuItem from './MenuItem'; + +class SearchMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + onPress + } = this.props; + + onPress(name); + } + + // + // Render + + render() { + const { + children, + ...otherProps + } = this.props; + + return ( + +
+ {children} +
+
+ ); + } +} + +SearchMenuItem.propTypes = { + name: PropTypes.string, + children: PropTypes.node.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default SearchMenuItem; diff --git a/frontend/src/Movie/Index/Menus/MovieIndexSearchMenu.js b/frontend/src/Movie/Index/Menus/MovieIndexSearchMenu.js new file mode 100644 index 000000000..c3cee3a04 --- /dev/null +++ b/frontend/src/Movie/Index/Menus/MovieIndexSearchMenu.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { align, icons } from 'Helpers/Props'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; +import SearchMenuItem from 'Components/Menu/SearchMenuItem'; + +class MovieIndexSearchMenu extends Component { + + render() { + const { + isDisabled, + onSearchPress + } = this.props; + + return ( + + + + + Search Missing + + + + Search Cutoff Unmet + + + + ); + } +} + +MovieIndexSearchMenu.propTypes = { + isDisabled: PropTypes.bool.isRequired, + onSearchPress: PropTypes.func.isRequired +}; + +export default MovieIndexSearchMenu; diff --git a/frontend/src/Movie/Index/MovieIndex.js b/frontend/src/Movie/Index/MovieIndex.js index 16314156b..2bdd6c007 100644 --- a/frontend/src/Movie/Index/MovieIndex.js +++ b/frontend/src/Movie/Index/MovieIndex.js @@ -5,7 +5,7 @@ import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import getSelectedIds from 'Utilities/Table/getSelectedIds'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; -import { align, icons, sortDirections } from 'Helpers/Props'; +import { align, icons, kinds, sortDirections } from 'Helpers/Props'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; @@ -15,6 +15,7 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; import NoMovie from 'Movie/NoMovie'; import MovieIndexTableConnector from './Table/MovieIndexTableConnector'; import MovieIndexTableOptionsConnector from './Table/MovieIndexTableOptionsConnector'; @@ -24,6 +25,7 @@ import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOvervie import MovieIndexOverviewsConnector from './Overview/MovieIndexOverviewsConnector'; import MovieIndexFilterMenu from './Menus/MovieIndexFilterMenu'; import MovieIndexSortMenu from './Menus/MovieIndexSortMenu'; +import MovieIndexSearchMenu from './Menus/MovieIndexSearchMenu'; import MovieIndexViewMenu from './Menus/MovieIndexViewMenu'; import MovieIndexFooterConnector from './MovieIndexFooterConnector'; import MovieEditorFooter from 'Movie/Editor/MovieEditorFooter.js'; @@ -60,6 +62,8 @@ class MovieIndex extends Component { isInteractiveImportModalOpen: false, isMovieEditorActive: false, isOrganizingMovieModalOpen: false, + isConfirmSearchModalOpen: false, + searchType: null, allSelected: false, allUnselected: false, lastToggled: null, @@ -215,7 +219,7 @@ class MovieIndex extends Component { if (this.state.isMovieEditorActive) { this.setState({ isMovieEditorActive: false }); } else { - const newState = selectAll(this.state.selectedState, false) + const newState = selectAll(this.state.selectedState, false); newState.isMovieEditorActive = true; this.setState(newState); } @@ -258,6 +262,19 @@ class MovieIndex extends Component { } } + onSearchPress = (command) => { + this.setState({ isConfirmSearchModalOpen: true, searchType: command }); + } + + onSearchConfirmed = () => { + this.props.onSearchPress(this.state.searchType); + this.setState({ isConfirmSearchModalOpen: false }); + } + + onConfirmSearchModalClose = () => { + this.setState({ isConfirmSearchModalOpen: false }); + } + onRender = () => { this.setState({ isRendered: true }, () => { const { @@ -299,6 +316,7 @@ class MovieIndex extends Component { isRefreshingMovie, isRssSyncExecuting, isOrganizingMovie, + isSearchingMovies, isSaving, saveError, isDeleting, @@ -309,6 +327,7 @@ class MovieIndex extends Component { onViewSelect, onRefreshMoviePress, onRssSyncPress, + onSearchPress, ...otherProps } = this.props; @@ -319,6 +338,7 @@ class MovieIndex extends Component { isPosterOptionsModalOpen, isOverviewOptionsModalOpen, isInteractiveImportModalOpen, + isConfirmSearchModalOpen, isMovieEditorActive, isRendered, selectedState, @@ -355,10 +375,9 @@ class MovieIndex extends Component { - + + +
+ Are you sure you want to perform mass movie search? +
+
+ This cannot be cancelled once started without restarting Radarr. +
+ + } + confirmLabel="Search" + onConfirm={this.onSearchConfirmed} + onCancel={this.onConfirmSearchModalClose} + /> ); } @@ -584,6 +622,7 @@ MovieIndex.propTypes = { view: PropTypes.string.isRequired, isRefreshingMovie: PropTypes.bool.isRequired, isOrganizingMovie: PropTypes.bool.isRequired, + isSearchingMovies: PropTypes.bool.isRequired, isRssSyncExecuting: PropTypes.bool.isRequired, scrollTop: PropTypes.number.isRequired, isSmallScreen: PropTypes.bool.isRequired, @@ -596,6 +635,7 @@ MovieIndex.propTypes = { onViewSelect: PropTypes.func.isRequired, onRefreshMoviePress: PropTypes.func.isRequired, onRssSyncPress: PropTypes.func.isRequired, + onSearchPress: PropTypes.func.isRequired, onScroll: PropTypes.func.isRequired, onSaveSelected: PropTypes.func.isRequired }; diff --git a/frontend/src/Movie/Index/MovieIndexConnector.js b/frontend/src/Movie/Index/MovieIndexConnector.js index e6b94e053..0c361f9b5 100644 --- a/frontend/src/Movie/Index/MovieIndexConnector.js +++ b/frontend/src/Movie/Index/MovieIndexConnector.js @@ -43,12 +43,16 @@ function createMapStateToProps() { createCommandExecutingSelector(commandNames.REFRESH_MOVIE), createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RENAME_MOVIE), + createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH), + createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH), createDimensionsSelector(), ( movies, isRefreshingMovie, isRssSyncExecuting, isOrganizingMovie, + isCutoffMoviesSearch, + isMissingMoviesSearch, dimensionsState ) => { return { @@ -56,6 +60,7 @@ function createMapStateToProps() { isRefreshingMovie, isRssSyncExecuting, isOrganizingMovie, + isSearchingMovies: isCutoffMoviesSearch || isMissingMoviesSearch, isSmallScreen: dimensionsState.isSmallScreen }; } @@ -98,6 +103,12 @@ function createMapDispatchToProps(dispatch, props) { dispatch(executeCommand({ name: commandNames.RSS_SYNC })); + }, + + onSearchPress(command) { + dispatch(executeCommand({ + name: command + })); } }; } diff --git a/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs b/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs index 7936f3531..90e6b94ee 100644 --- a/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs @@ -90,7 +90,6 @@ public void Execute(CutoffUnmetMoviesSearchCommand message) List movies = _movieCutoffService.MoviesWhereCutoffUnmet(pagingSpec).Records.ToList(); - var queue = _queueService.GetQueue().Select(q => q.Movie.Id); var missing = movies.Where(e => !queue.Contains(e.Id)).ToList(); diff --git a/src/NzbDrone.Core/Movies/MovieRepository.cs b/src/NzbDrone.Core/Movies/MovieRepository.cs index 35095c139..f28d89bb4 100644 --- a/src/NzbDrone.Core/Movies/MovieRepository.cs +++ b/src/NzbDrone.Core/Movies/MovieRepository.cs @@ -177,7 +177,9 @@ public PagingSpec MoviesWhereCutoffUnmet(PagingSpec pagingSpec, Li private SortBuilder MoviesWhereCutoffUnmetQuery(PagingSpec pagingSpec, List qualitiesBelowCutoff) { - return Query.Where(pagingSpec.FilterExpressions.FirstOrDefault()) + return Query + .Join(JoinType.Left, e => e.MovieFile, (e, s) => e.MovieFileId == s.Id) + .Where(pagingSpec.FilterExpressions.FirstOrDefault()) .AndWhere(m => m.MovieFileId != 0) .AndWhere(BuildQualityCutoffWhereClause(qualitiesBelowCutoff)) .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection())