mirror of
https://github.com/Radarr/Radarr.git
synced 2024-10-05 15:47:20 +02:00
New: Wanted Cutoff/Missing
This commit is contained in:
parent
9798202589
commit
152f50a1ef
@ -33,6 +33,8 @@ import Status from 'System/Status/Status';
|
|||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||||
|
|
||||||
function AppRoutes(props) {
|
function AppRoutes(props) {
|
||||||
const {
|
const {
|
||||||
@ -121,6 +123,20 @@ function AppRoutes(props) {
|
|||||||
component={BlocklistConnector}
|
component={BlocklistConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Wanted
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/wanted/missing"
|
||||||
|
component={MissingConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/wanted/cutoffunmet"
|
||||||
|
component={CutoffUnmetConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Settings
|
Settings
|
||||||
*/}
|
*/}
|
||||||
|
@ -71,6 +71,22 @@ const links = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
iconName: icons.WARNING,
|
||||||
|
title: () => translate('Wanted'),
|
||||||
|
to: '/wanted/missing',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: () => translate('Missing'),
|
||||||
|
to: '/wanted/missing'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: () => translate('CutoffUnmet'),
|
||||||
|
to: '/wanted/cutoffunmet'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.SETTINGS,
|
iconName: icons.SETTINGS,
|
||||||
title: () => translate('Settings'),
|
title: () => translate('Settings'),
|
||||||
|
@ -244,6 +244,26 @@ class SignalRConnector extends Component {
|
|||||||
this.props.dispatchSetVersion({ version });
|
this.props.dispatchSetVersion({ version });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleWantedCutoff = (body) => {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({
|
||||||
|
section: 'wanted.cutoffUnmet',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleWantedMissing = (body) => {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({
|
||||||
|
section: 'wanted.missing',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleSystemTask = () => {
|
handleSystemTask = () => {
|
||||||
this.props.dispatchFetchCommands();
|
this.props.dispatchFetchCommands();
|
||||||
};
|
};
|
||||||
|
6
frontend/src/Movie/MovieSearchCell.css
Normal file
6
frontend/src/Movie/MovieSearchCell.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.movieSearchCell {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 70px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
7
frontend/src/Movie/MovieSearchCell.css.d.ts
vendored
Normal file
7
frontend/src/Movie/MovieSearchCell.css.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'movieSearchCell': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
81
frontend/src/Movie/MovieSearchCell.js
Normal file
81
frontend/src/Movie/MovieSearchCell.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import MovieInteractiveSearchModalConnector from './Search/MovieInteractiveSearchModalConnector';
|
||||||
|
import styles from './MovieSearchCell.css';
|
||||||
|
|
||||||
|
class MovieSearchCell extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isInteractiveSearchModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onManualSearchPress = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveSearchModalClose = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
movieId,
|
||||||
|
movieTitle,
|
||||||
|
isSearching,
|
||||||
|
onSearchPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.movieSearchCell}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.SEARCH}
|
||||||
|
isSpinning={isSearching}
|
||||||
|
onPress={onSearchPress}
|
||||||
|
title={translate('AutomaticSearch')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
onPress={this.onManualSearchPress}
|
||||||
|
title={translate('InteractiveSearch')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MovieInteractiveSearchModalConnector
|
||||||
|
isOpen={this.state.isInteractiveSearchModalOpen}
|
||||||
|
movieId={movieId}
|
||||||
|
movieTitle={movieTitle}
|
||||||
|
onModalClose={this.onInteractiveSearchModalClose}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieSearchCell.propTypes = {
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieTitle: PropTypes.string.isRequired,
|
||||||
|
isSearching: PropTypes.bool.isRequired,
|
||||||
|
onSearchPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieSearchCell;
|
48
frontend/src/Movie/MovieSearchCellConnector.js
Normal file
48
frontend/src/Movie/MovieSearchCellConnector.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
|
import { isCommandExecuting } from 'Utilities/Command';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { movieId }) => movieId,
|
||||||
|
createMovieSelector(),
|
||||||
|
createCommandsSelector(),
|
||||||
|
(movieId, movie, commands) => {
|
||||||
|
const isSearching = commands.some((command) => {
|
||||||
|
const movieSearch = command.name === commandNames.MOVIE_SEARCH;
|
||||||
|
|
||||||
|
if (!movieSearch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
isCommandExecuting(command) &&
|
||||||
|
command.body.movieIds.indexOf(movieId) > -1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
movieMonitored: movie.monitored,
|
||||||
|
isSearching
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onSearchPress(name, path) {
|
||||||
|
dispatch(executeCommand({
|
||||||
|
name: commandNames.MOVIE_SEARCH,
|
||||||
|
movieIds: [props.movieId]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieSearchCell);
|
4
frontend/src/Movie/MovieStatus.css
Normal file
4
frontend/src/Movie/MovieStatus.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
7
frontend/src/Movie/MovieStatus.css.d.ts
vendored
Normal file
7
frontend/src/Movie/MovieStatus.css.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'center': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
115
frontend/src/Movie/MovieStatus.js
Normal file
115
frontend/src/Movie/MovieStatus.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import MovieQuality from './MovieQuality';
|
||||||
|
import styles from './MovieStatus.css';
|
||||||
|
|
||||||
|
function MovieStatus(props) {
|
||||||
|
const {
|
||||||
|
isAvailable,
|
||||||
|
monitored,
|
||||||
|
grabbed,
|
||||||
|
queueItem,
|
||||||
|
movieFile
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const hasMovieFile = !!movieFile;
|
||||||
|
const isQueued = !!queueItem;
|
||||||
|
|
||||||
|
if (isQueued) {
|
||||||
|
const {
|
||||||
|
sizeleft,
|
||||||
|
size
|
||||||
|
} = queueItem;
|
||||||
|
|
||||||
|
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<QueueDetails
|
||||||
|
{...queueItem}
|
||||||
|
progressBar={
|
||||||
|
<ProgressBar
|
||||||
|
progress={progress}
|
||||||
|
kind={kinds.PURPLE}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grabbed) {
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
title={translate('MovieIsDownloading')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMovieFile) {
|
||||||
|
const quality = movieFile.quality;
|
||||||
|
const isCutoffNotMet = movieFile.qualityCutoffNotMet;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<MovieQuality
|
||||||
|
quality={quality}
|
||||||
|
size={movieFile.size}
|
||||||
|
isCutoffNotMet={isCutoffNotMet}
|
||||||
|
title={translate('MovieDownloaded')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monitored) {
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.UNMONITORED}
|
||||||
|
kind={kinds.DISABLED}
|
||||||
|
title={translate('MovieIsNotMonitored')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAvailable) {
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.MISSING}
|
||||||
|
title={translate('MovieMissingFromDisk')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.NOT_AIRED}
|
||||||
|
title={translate('MovieIsNotAvailable')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieStatus.propTypes = {
|
||||||
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
grabbed: PropTypes.bool,
|
||||||
|
queueItem: PropTypes.object,
|
||||||
|
movieFile: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieStatus;
|
50
frontend/src/Movie/MovieStatusConnector.js
Normal file
50
frontend/src/Movie/MovieStatusConnector.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import MovieStatus from 'Movie/MovieStatus';
|
||||||
|
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
||||||
|
import { createMovieByEntitySelector } from 'Store/Selectors/createMovieSelector';
|
||||||
|
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createMovieByEntitySelector(),
|
||||||
|
createQueueItemSelector(),
|
||||||
|
createMovieFileSelector(),
|
||||||
|
(movie, queueItem, movieFile) => {
|
||||||
|
const result = _.pick(movie, [
|
||||||
|
'isAvailable',
|
||||||
|
'monitored',
|
||||||
|
'grabbed'
|
||||||
|
]);
|
||||||
|
|
||||||
|
result.queueItem = queueItem;
|
||||||
|
result.movieFile = movieFile;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MovieStatusConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<MovieStatus
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieStatusConnector.propTypes = {
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieFileId: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, null)(MovieStatusConnector);
|
@ -8,6 +8,7 @@ function MovieInteractiveSearchModal(props) {
|
|||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
movieId,
|
movieId,
|
||||||
|
movieTitle,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ function MovieInteractiveSearchModal(props) {
|
|||||||
>
|
>
|
||||||
<MovieInteractiveSearchModalContent
|
<MovieInteractiveSearchModalContent
|
||||||
movieId={movieId}
|
movieId={movieId}
|
||||||
|
movieTitle={movieTitle}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -29,6 +31,7 @@ function MovieInteractiveSearchModal(props) {
|
|||||||
MovieInteractiveSearchModal.propTypes = {
|
MovieInteractiveSearchModal.propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieTitle: PropTypes.string,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,13 +12,17 @@ import translate from 'Utilities/String/translate';
|
|||||||
function MovieInteractiveSearchModalContent(props) {
|
function MovieInteractiveSearchModalContent(props) {
|
||||||
const {
|
const {
|
||||||
movieId,
|
movieId,
|
||||||
|
movieTitle,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('InteractiveSearchModalHeader')}
|
{movieTitle === undefined ?
|
||||||
|
translate('InteractiveSearchModalHeader') :
|
||||||
|
translate('InteractiveSearchModalHeaderTitle', { title: movieTitle })
|
||||||
|
}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
@ -38,6 +42,7 @@ function MovieInteractiveSearchModalContent(props) {
|
|||||||
|
|
||||||
MovieInteractiveSearchModalContent.propTypes = {
|
MovieInteractiveSearchModalContent.propTypes = {
|
||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieTitle: PropTypes.string,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
export const CALENDAR = 'calendar';
|
export const CALENDAR = 'calendar';
|
||||||
export const MOVIES = 'movies';
|
export const MOVIES = 'movies';
|
||||||
export const INTERACTIVE_IMPORT = 'interactiveImport.movies';
|
export const INTERACTIVE_IMPORT = 'interactiveImport.movies';
|
||||||
|
export const WANTED_CUTOFF_UNMET = 'wanted.cutoffUnmet';
|
||||||
|
export const WANTED_MISSING = 'wanted.missing';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
CALENDAR,
|
CALENDAR,
|
||||||
MOVIES,
|
MOVIES,
|
||||||
INTERACTIVE_IMPORT
|
INTERACTIVE_IMPORT,
|
||||||
|
WANTED_CUTOFF_UNMET,
|
||||||
|
WANTED_MISSING
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,7 @@ function createMapStateToProps() {
|
|||||||
createMovieFileSelector(),
|
createMovieFileSelector(),
|
||||||
(movieFile) => {
|
(movieFile) => {
|
||||||
return {
|
return {
|
||||||
language: movieFile ? movieFile.language : undefined
|
languages: movieFile ? movieFile.languages : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,29 +1,29 @@
|
|||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import updateEpisodes from 'Utilities/Episode/updateEpisodes';
|
import updateMovies from 'Utilities/Movie/updateMovies';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
|
||||||
function createBatchToggleEpisodeMonitoredHandler(section, fetchHandler) {
|
function createBatchToggleMovieMonitoredHandler(section, fetchHandler) {
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
const {
|
const {
|
||||||
episodeIds,
|
movieIds,
|
||||||
monitored
|
monitored
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const state = getSectionState(getState(), section, true);
|
const state = getSectionState(getState(), section, true);
|
||||||
|
|
||||||
dispatch(updateEpisodes(section, state.items, episodeIds, {
|
dispatch(updateMovies(section, state.items, movieIds, {
|
||||||
isSaving: true
|
isSaving: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url: '/episode/monitor',
|
url: '/movie/editor',
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: JSON.stringify({ episodeIds, monitored }),
|
data: JSON.stringify({ movieIds, monitored }),
|
||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
}).request;
|
}).request;
|
||||||
|
|
||||||
promise.done(() => {
|
promise.done(() => {
|
||||||
dispatch(updateEpisodes(section, state.items, episodeIds, {
|
dispatch(updateMovies(section, state.items, movieIds, {
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
monitored
|
monitored
|
||||||
}));
|
}));
|
||||||
@ -32,11 +32,11 @@ function createBatchToggleEpisodeMonitoredHandler(section, fetchHandler) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
promise.fail(() => {
|
promise.fail(() => {
|
||||||
dispatch(updateEpisodes(section, state.items, episodeIds, {
|
dispatch(updateMovies(section, state.items, movieIds, {
|
||||||
isSaving: false
|
isSaving: false
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createBatchToggleEpisodeMonitoredHandler;
|
export default createBatchToggleMovieMonitoredHandler;
|
@ -28,6 +28,7 @@ import * as rootFolders from './rootFolderActions';
|
|||||||
import * as settings from './settingsActions';
|
import * as settings from './settingsActions';
|
||||||
import * as system from './systemActions';
|
import * as system from './systemActions';
|
||||||
import * as tags from './tagActions';
|
import * as tags from './tagActions';
|
||||||
|
import * as wanted from './wantedActions';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
addMovie,
|
addMovie,
|
||||||
@ -59,5 +60,6 @@ export default [
|
|||||||
movieCredits,
|
movieCredits,
|
||||||
settings,
|
settings,
|
||||||
system,
|
system,
|
||||||
tags
|
tags,
|
||||||
|
wanted
|
||||||
];
|
];
|
||||||
|
298
frontend/src/Store/Actions/wantedActions.js
Normal file
298
frontend/src/Store/Actions/wantedActions.js
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { filterTypes, sortDirections } from 'Helpers/Props';
|
||||||
|
import createBatchToggleMovieMonitoredHandler from 'Store/Actions/Creators/createBatchToggleMovieMonitoredHandler';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
|
||||||
|
import createClearReducer from './Creators/Reducers/createClearReducer';
|
||||||
|
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'wanted';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
missing: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
pageSize: 20,
|
||||||
|
sortKey: 'movieMetadata.sortTitle',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'movieMetadata.sortTitle',
|
||||||
|
label: () => translate('MovieTitle'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'movieMetadata.year',
|
||||||
|
label: () => translate('Year'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: () => translate('Status'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
columnLabel: () => translate('Actions'),
|
||||||
|
isVisible: true,
|
||||||
|
isModifiable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
selectedFilterKey: 'monitored',
|
||||||
|
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
label: () => translate('Monitored'),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
value: true,
|
||||||
|
type: filterTypes.EQUAL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'unmonitored',
|
||||||
|
label: () => translate('Unmonitored'),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
value: false,
|
||||||
|
type: filterTypes.EQUAL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
cutoffUnmet: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
pageSize: 20,
|
||||||
|
sortKey: 'movieMetadata.sortTitle',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
items: [],
|
||||||
|
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'movieMetadata.sortTitle',
|
||||||
|
label: () => translate('MovieTitle'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'movieMetadata.year',
|
||||||
|
label: () => translate('Year'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'languages',
|
||||||
|
label: () => translate('Languages'),
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: () => translate('Status'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
columnLabel: () => translate('Actions'),
|
||||||
|
isVisible: true,
|
||||||
|
isModifiable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
selectedFilterKey: 'monitored',
|
||||||
|
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
label: () => translate('Monitored'),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
value: true,
|
||||||
|
type: filterTypes.EQUAL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'unmonitored',
|
||||||
|
label: () => translate('Unmonitored'),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
value: false,
|
||||||
|
type: filterTypes.EQUAL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const persistState = [
|
||||||
|
'wanted.missing.pageSize',
|
||||||
|
'wanted.missing.sortKey',
|
||||||
|
'wanted.missing.sortDirection',
|
||||||
|
'wanted.missing.selectedFilterKey',
|
||||||
|
'wanted.missing.columns',
|
||||||
|
'wanted.cutoffUnmet.pageSize',
|
||||||
|
'wanted.cutoffUnmet.sortKey',
|
||||||
|
'wanted.cutoffUnmet.sortDirection',
|
||||||
|
'wanted.cutoffUnmet.selectedFilterKey',
|
||||||
|
'wanted.cutoffUnmet.columns'
|
||||||
|
];
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_MISSING = 'wanted/missing/fetchMissing';
|
||||||
|
export const GOTO_FIRST_MISSING_PAGE = 'wanted/missing/gotoMissingFirstPage';
|
||||||
|
export const GOTO_PREVIOUS_MISSING_PAGE = 'wanted/missing/gotoMissingPreviousPage';
|
||||||
|
export const GOTO_NEXT_MISSING_PAGE = 'wanted/missing/gotoMissingNextPage';
|
||||||
|
export const GOTO_LAST_MISSING_PAGE = 'wanted/missing/gotoMissingLastPage';
|
||||||
|
export const GOTO_MISSING_PAGE = 'wanted/missing/gotoMissingPage';
|
||||||
|
export const SET_MISSING_SORT = 'wanted/missing/setMissingSort';
|
||||||
|
export const SET_MISSING_FILTER = 'wanted/missing/setMissingFilter';
|
||||||
|
export const SET_MISSING_TABLE_OPTION = 'wanted/missing/setMissingTableOption';
|
||||||
|
export const CLEAR_MISSING = 'wanted/missing/clearMissing';
|
||||||
|
|
||||||
|
export const BATCH_TOGGLE_MISSING_MOVIES = 'wanted/missing/batchToggleMissingMovies';
|
||||||
|
|
||||||
|
export const FETCH_CUTOFF_UNMET = 'wanted/cutoffUnmet/fetchCutoffUnmet';
|
||||||
|
export const GOTO_FIRST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFirstPage';
|
||||||
|
export const GOTO_PREVIOUS_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPreviousPage';
|
||||||
|
export const GOTO_NEXT_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetNextPage';
|
||||||
|
export const GOTO_LAST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFastPage';
|
||||||
|
export const GOTO_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPage';
|
||||||
|
export const SET_CUTOFF_UNMET_SORT = 'wanted/cutoffUnmet/setCutoffUnmetSort';
|
||||||
|
export const SET_CUTOFF_UNMET_FILTER = 'wanted/cutoffUnmet/setCutoffUnmetFilter';
|
||||||
|
export const SET_CUTOFF_UNMET_TABLE_OPTION = 'wanted/cutoffUnmet/setCutoffUnmetTableOption';
|
||||||
|
export const CLEAR_CUTOFF_UNMET = 'wanted/cutoffUnmet/clearCutoffUnmet';
|
||||||
|
|
||||||
|
export const BATCH_TOGGLE_CUTOFF_UNMET_MOVIES = 'wanted/cutoffUnmet/batchToggleCutoffUnmetMovies';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchMissing = createThunk(FETCH_MISSING);
|
||||||
|
export const gotoMissingFirstPage = createThunk(GOTO_FIRST_MISSING_PAGE);
|
||||||
|
export const gotoMissingPreviousPage = createThunk(GOTO_PREVIOUS_MISSING_PAGE);
|
||||||
|
export const gotoMissingNextPage = createThunk(GOTO_NEXT_MISSING_PAGE);
|
||||||
|
export const gotoMissingLastPage = createThunk(GOTO_LAST_MISSING_PAGE);
|
||||||
|
export const gotoMissingPage = createThunk(GOTO_MISSING_PAGE);
|
||||||
|
export const setMissingSort = createThunk(SET_MISSING_SORT);
|
||||||
|
export const setMissingFilter = createThunk(SET_MISSING_FILTER);
|
||||||
|
export const setMissingTableOption = createAction(SET_MISSING_TABLE_OPTION);
|
||||||
|
export const clearMissing = createAction(CLEAR_MISSING);
|
||||||
|
|
||||||
|
export const batchToggleMissingMovies = createThunk(BATCH_TOGGLE_MISSING_MOVIES);
|
||||||
|
|
||||||
|
export const fetchCutoffUnmet = createThunk(FETCH_CUTOFF_UNMET);
|
||||||
|
export const gotoCutoffUnmetFirstPage = createThunk(GOTO_FIRST_CUTOFF_UNMET_PAGE);
|
||||||
|
export const gotoCutoffUnmetPreviousPage = createThunk(GOTO_PREVIOUS_CUTOFF_UNMET_PAGE);
|
||||||
|
export const gotoCutoffUnmetNextPage = createThunk(GOTO_NEXT_CUTOFF_UNMET_PAGE);
|
||||||
|
export const gotoCutoffUnmetLastPage = createThunk(GOTO_LAST_CUTOFF_UNMET_PAGE);
|
||||||
|
export const gotoCutoffUnmetPage = createThunk(GOTO_CUTOFF_UNMET_PAGE);
|
||||||
|
export const setCutoffUnmetSort = createThunk(SET_CUTOFF_UNMET_SORT);
|
||||||
|
export const setCutoffUnmetFilter = createThunk(SET_CUTOFF_UNMET_FILTER);
|
||||||
|
export const setCutoffUnmetTableOption = createAction(SET_CUTOFF_UNMET_TABLE_OPTION);
|
||||||
|
export const clearCutoffUnmet = createAction(CLEAR_CUTOFF_UNMET);
|
||||||
|
|
||||||
|
export const batchToggleCutoffUnmetMovies = createThunk(BATCH_TOGGLE_CUTOFF_UNMET_MOVIES);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
|
...createServerSideCollectionHandlers(
|
||||||
|
'wanted.missing',
|
||||||
|
'/wanted/missing',
|
||||||
|
fetchMissing,
|
||||||
|
{
|
||||||
|
[serverSideCollectionHandlers.FETCH]: FETCH_MISSING,
|
||||||
|
[serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.SORT]: SET_MISSING_SORT,
|
||||||
|
[serverSideCollectionHandlers.FILTER]: SET_MISSING_FILTER
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
[BATCH_TOGGLE_MISSING_MOVIES]: createBatchToggleMovieMonitoredHandler('wanted.missing', fetchMissing),
|
||||||
|
|
||||||
|
...createServerSideCollectionHandlers(
|
||||||
|
'wanted.cutoffUnmet',
|
||||||
|
'/wanted/cutoff',
|
||||||
|
fetchCutoffUnmet,
|
||||||
|
{
|
||||||
|
[serverSideCollectionHandlers.FETCH]: FETCH_CUTOFF_UNMET,
|
||||||
|
[serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.SORT]: SET_CUTOFF_UNMET_SORT,
|
||||||
|
[serverSideCollectionHandlers.FILTER]: SET_CUTOFF_UNMET_FILTER
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
[BATCH_TOGGLE_CUTOFF_UNMET_MOVIES]: createBatchToggleMovieMonitoredHandler('wanted.cutoffUnmet', fetchCutoffUnmet)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
[SET_MISSING_TABLE_OPTION]: createSetTableOptionReducer('wanted.missing'),
|
||||||
|
[SET_CUTOFF_UNMET_TABLE_OPTION]: createSetTableOptionReducer('wanted.cutoffUnmet'),
|
||||||
|
|
||||||
|
[CLEAR_MISSING]: createClearReducer(
|
||||||
|
'wanted.missing',
|
||||||
|
{
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
totalPages: 0,
|
||||||
|
totalRecords: 0
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
[CLEAR_CUTOFF_UNMET]: createClearReducer(
|
||||||
|
'wanted.cutoffUnmet',
|
||||||
|
{
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
totalPages: 0,
|
||||||
|
totalRecords: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}, defaultState, section);
|
@ -1,4 +1,6 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import movieEntities from 'Movie/movieEntities';
|
||||||
|
|
||||||
export function createMovieSelectorForHook(movieId) {
|
export function createMovieSelectorForHook(movieId) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
@ -11,6 +13,16 @@ export function createMovieSelectorForHook(movieId) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createMovieByEntitySelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { movieId }) => movieId,
|
||||||
|
(state, { movieEntity = movieEntities.MOVIES }) => _.get(state, movieEntity, { items: [] }),
|
||||||
|
(movieId, movies) => {
|
||||||
|
return _.find(movies.items, { id: movieId });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function createMovieSelector() {
|
function createMovieSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { movieId }) => movieId,
|
(state, { movieId }) => movieId,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { update } from 'Store/Actions/baseActions';
|
import { update } from 'Store/Actions/baseActions';
|
||||||
|
|
||||||
function updateEpisodes(section, episodes, episodeIds, options) {
|
function updateMovies(section, movies, movieIds, options) {
|
||||||
const data = _.reduce(episodes, (result, item) => {
|
const data = _.reduce(movies, (result, item) => {
|
||||||
if (episodeIds.indexOf(item.id) > -1) {
|
if (movieIds.indexOf(item.id) > -1) {
|
||||||
result.push({
|
result.push({
|
||||||
...item,
|
...item,
|
||||||
...options
|
...options
|
||||||
@ -18,4 +18,4 @@ function updateEpisodes(section, episodes, episodeIds, options) {
|
|||||||
return update({ section, data });
|
return update({ section, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default updateEpisodes;
|
export default updateMovies;
|
301
frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js
Normal file
301
frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import CutoffUnmetRow from './CutoffUnmetRow';
|
||||||
|
|
||||||
|
function getMonitoredValue(props) {
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
selectedFilterKey
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CutoffUnmet extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {},
|
||||||
|
isConfirmSearchAllCutoffUnmetModalOpen: false,
|
||||||
|
isInteractiveImportModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
this.setState((state) => {
|
||||||
|
return removeOldSelectedState(state, prevProps.items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFilterMenuItemPress = (filterKey, filterValue) => {
|
||||||
|
this.props.onFilterSelect(filterKey, filterValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchSelectedPress = () => {
|
||||||
|
const selected = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.onSearchSelectedPress(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
onToggleSelectedPress = () => {
|
||||||
|
const movieIds = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.batchToggleCutoffUnmetMovies({
|
||||||
|
movieIds,
|
||||||
|
monitored: !getMonitoredValue(this.props)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllCutoffUnmetPress = () => {
|
||||||
|
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllCutoffUnmetConfirmed = () => {
|
||||||
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
|
onSearchAllCutoffUnmetPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// TODO: Custom filters will need to check whether there is a monitored
|
||||||
|
// filter once implemented.
|
||||||
|
|
||||||
|
onSearchAllCutoffUnmetPress(selectedFilterKey === 'monitored');
|
||||||
|
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmSearchAllCutoffUnmetModalClose = () => {
|
||||||
|
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
columns,
|
||||||
|
totalRecords,
|
||||||
|
isSearchingForCutoffUnmetMovies,
|
||||||
|
isSaving,
|
||||||
|
onFilterSelect,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState,
|
||||||
|
isConfirmSearchAllCutoffUnmetModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const itemsSelected = !!this.getSelectedIds().length;
|
||||||
|
const isShowingMonitored = getMonitoredValue(this.props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('CutoffUnmet')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchSelected')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!itemsSelected || isSearchingForCutoffUnmetMovies}
|
||||||
|
onPress={this.onSearchSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||||
|
iconName={icons.MONITORED}
|
||||||
|
isDisabled={!itemsSelected}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
onPress={this.onToggleSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchAll')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!items.length}
|
||||||
|
isSpinning={isSearchingForCutoffUnmetMovies}
|
||||||
|
onPress={this.onSearchAllCutoffUnmetPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
{...otherProps}
|
||||||
|
columns={columns}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={[]}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{
|
||||||
|
isFetching && !isPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && error &&
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('CutoffUnmetLoadError')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !items.length &&
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{translate('CutoffUnmetNoItems')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !!items.length &&
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
{...otherProps}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<CutoffUnmetRow
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmSearchAllCutoffUnmetModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('SearchForCutoffUnmetMovies')}
|
||||||
|
message={
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{translate('SearchForCutoffUnmetMoviesConfirmationCount', { totalRecords })}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{translate('MassSearchCancelWarning')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
confirmLabel={translate('Search')}
|
||||||
|
onConfirm={this.onSearchAllCutoffUnmetConfirmed}
|
||||||
|
onCancel={this.onConfirmSearchAllCutoffUnmetModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CutoffUnmet.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
totalRecords: PropTypes.number,
|
||||||
|
isSearchingForCutoffUnmetMovies: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
onSearchSelectedPress: PropTypes.func.isRequired,
|
||||||
|
batchToggleCutoffUnmetMovies: PropTypes.func.isRequired,
|
||||||
|
onSearchAllCutoffUnmetPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CutoffUnmet;
|
185
frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js
Normal file
185
frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||||
|
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
|
import * as wantedActions from 'Store/Actions/wantedActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import CutoffUnmet from './CutoffUnmet';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.wanted.cutoffUnmet,
|
||||||
|
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH),
|
||||||
|
(cutoffUnmet, isSearchingForCutoffUnmetMovies) => {
|
||||||
|
return {
|
||||||
|
isSearchingForCutoffUnmetMovies,
|
||||||
|
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
|
||||||
|
...cutoffUnmet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...wantedActions,
|
||||||
|
executeCommand,
|
||||||
|
fetchQueueDetails,
|
||||||
|
clearQueueDetails,
|
||||||
|
fetchMovieFiles,
|
||||||
|
clearMovieFiles
|
||||||
|
};
|
||||||
|
|
||||||
|
class CutoffUnmetConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchCutoffUnmet,
|
||||||
|
gotoCutoffUnmetFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchCutoffUnmet();
|
||||||
|
} else {
|
||||||
|
gotoCutoffUnmetFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
const movieIds = selectUniqueIds(this.props.items, 'id');
|
||||||
|
const movieFileIds = selectUniqueIds(this.props.items, 'movieFileId');
|
||||||
|
|
||||||
|
this.props.fetchQueueDetails({ movieIds });
|
||||||
|
|
||||||
|
if (movieFileIds.length) {
|
||||||
|
this.props.fetchMovieFiles({ movieFileIds });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
this.props.clearCutoffUnmet();
|
||||||
|
this.props.clearQueueDetails();
|
||||||
|
this.props.clearMovieFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchCutoffUnmet();
|
||||||
|
};
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFirstPagePress = () => {
|
||||||
|
this.props.gotoCutoffUnmetFirstPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPreviousPagePress = () => {
|
||||||
|
this.props.gotoCutoffUnmetPreviousPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onNextPagePress = () => {
|
||||||
|
this.props.gotoCutoffUnmetNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onLastPagePress = () => {
|
||||||
|
this.props.gotoCutoffUnmetLastPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPageSelect = (page) => {
|
||||||
|
this.props.gotoCutoffUnmetPage({ page });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.setCutoffUnmetSort({ sortKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setCutoffUnmetFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setCutoffUnmetTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoCutoffUnmetFirstPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchSelectedPress = (selected) => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.MOVIE_SEARCH,
|
||||||
|
movieIds: selected
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllCutoffUnmetPress = (monitored) => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CutoffUnmet
|
||||||
|
onFirstPagePress={this.onFirstPagePress}
|
||||||
|
onPreviousPagePress={this.onPreviousPagePress}
|
||||||
|
onNextPagePress={this.onNextPagePress}
|
||||||
|
onLastPagePress={this.onLastPagePress}
|
||||||
|
onPageSelect={this.onPageSelect}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onSearchSelectedPress={this.onSearchSelectedPress}
|
||||||
|
onToggleSelectedPress={this.onToggleSelectedPress}
|
||||||
|
onSearchAllCutoffUnmetPress={this.onSearchAllCutoffUnmetPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CutoffUnmetConnector.propTypes = {
|
||||||
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchCutoffUnmet: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetFirstPage: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetPreviousPage: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetNextPage: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetLastPage: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetPage: PropTypes.func.isRequired,
|
||||||
|
setCutoffUnmetSort: PropTypes.func.isRequired,
|
||||||
|
setCutoffUnmetFilter: PropTypes.func.isRequired,
|
||||||
|
setCutoffUnmetTableOption: PropTypes.func.isRequired,
|
||||||
|
clearCutoffUnmet: PropTypes.func.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired,
|
||||||
|
fetchQueueDetails: PropTypes.func.isRequired,
|
||||||
|
clearQueueDetails: PropTypes.func.isRequired,
|
||||||
|
fetchMovieFiles: PropTypes.func.isRequired,
|
||||||
|
clearMovieFiles: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(CutoffUnmetConnector)
|
||||||
|
);
|
6
frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css
Normal file
6
frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.languages,
|
||||||
|
.status {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 100px;
|
||||||
|
}
|
8
frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css.d.ts
vendored
Normal file
8
frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'languages': string;
|
||||||
|
'status': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
120
frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js
Normal file
120
frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import movieEntities from 'Movie/movieEntities';
|
||||||
|
import MovieSearchCellConnector from 'Movie/MovieSearchCellConnector';
|
||||||
|
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import MovieFileLanguageConnector from 'MovieFile/MovieFileLanguageConnector';
|
||||||
|
import styles from './CutoffUnmetRow.css';
|
||||||
|
|
||||||
|
function CutoffUnmetRow(props) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
movieFileId,
|
||||||
|
year,
|
||||||
|
title,
|
||||||
|
titleSlug,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieTitleLink
|
||||||
|
titleSlug={titleSlug}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.year') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{year}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.languages}
|
||||||
|
>
|
||||||
|
<MovieFileLanguageConnector
|
||||||
|
movieFileId={movieFileId}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.status}
|
||||||
|
>
|
||||||
|
<MovieStatusConnector
|
||||||
|
movieId={id}
|
||||||
|
movieFileId={movieFileId}
|
||||||
|
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<MovieSearchCellConnector
|
||||||
|
key={name}
|
||||||
|
movieId={id}
|
||||||
|
movieTitle={title}
|
||||||
|
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CutoffUnmetRow.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
movieFileId: PropTypes.number,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
titleSlug: PropTypes.string.isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CutoffUnmetRow;
|
319
frontend/src/Wanted/Missing/Missing.js
Normal file
319
frontend/src/Wanted/Missing/Missing.js
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
|
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import MissingRow from './MissingRow';
|
||||||
|
|
||||||
|
function getMonitoredValue(props) {
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
selectedFilterKey
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Missing extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {},
|
||||||
|
isConfirmSearchAllMissingModalOpen: false,
|
||||||
|
isInteractiveImportModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
this.setState((state) => {
|
||||||
|
return removeOldSelectedState(state, prevProps.items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchSelectedPress = () => {
|
||||||
|
const selected = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.onSearchSelectedPress(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
onToggleSelectedPress = () => {
|
||||||
|
const movieIds = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.batchToggleMissingMovies({
|
||||||
|
movieIds,
|
||||||
|
monitored: !getMonitoredValue(this.props)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllMissingPress = () => {
|
||||||
|
this.setState({ isConfirmSearchAllMissingModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllMissingConfirmed = () => {
|
||||||
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
|
onSearchAllMissingPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// TODO: Custom filters will need to check whether there is a monitored
|
||||||
|
// filter once implemented.
|
||||||
|
|
||||||
|
onSearchAllMissingPress(selectedFilterKey === 'monitored');
|
||||||
|
this.setState({ isConfirmSearchAllMissingModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmSearchAllMissingModalClose = () => {
|
||||||
|
this.setState({ isConfirmSearchAllMissingModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveImportPress = () => {
|
||||||
|
this.setState({ isInteractiveImportModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveImportModalClose = () => {
|
||||||
|
this.setState({ isInteractiveImportModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
columns,
|
||||||
|
totalRecords,
|
||||||
|
isSearchingForMissingMovies,
|
||||||
|
isSaving,
|
||||||
|
onFilterSelect,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState,
|
||||||
|
isConfirmSearchAllMissingModalOpen,
|
||||||
|
isInteractiveImportModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const itemsSelected = !!this.getSelectedIds().length;
|
||||||
|
const isShowingMonitored = getMonitoredValue(this.props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Missing')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchSelected')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!itemsSelected || isSearchingForMissingMovies}
|
||||||
|
onPress={this.onSearchSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||||
|
iconName={icons.MONITORED}
|
||||||
|
isDisabled={!itemsSelected}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
onPress={this.onToggleSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchAll')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!items.length}
|
||||||
|
isSpinning={isSearchingForMissingMovies}
|
||||||
|
onPress={this.onSearchAllMissingPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('ManualImport')}
|
||||||
|
iconName={icons.INTERACTIVE}
|
||||||
|
onPress={this.onInteractiveImportPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
{...otherProps}
|
||||||
|
columns={columns}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={[]}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{
|
||||||
|
isFetching && !isPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && error &&
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('MissingLoadError')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !items.length &&
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{translate('MissingNoItems')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !!items.length &&
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
{...otherProps}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<MissingRow
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmSearchAllMissingModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('SearchForAllMissingMovies')}
|
||||||
|
message={
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{translate('SearchForAllMissingMoviesConfirmationCount', { totalRecords })}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{translate('MassSearchCancelWarning')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
confirmLabel={translate('Search')}
|
||||||
|
onConfirm={this.onSearchAllMissingConfirmed}
|
||||||
|
onCancel={this.onConfirmSearchAllMissingModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<InteractiveImportModal
|
||||||
|
isOpen={isInteractiveImportModalOpen}
|
||||||
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
|
/>
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Missing.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
totalRecords: PropTypes.number,
|
||||||
|
isSearchingForMissingMovies: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
onSearchSelectedPress: PropTypes.func.isRequired,
|
||||||
|
batchToggleMissingMovies: PropTypes.func.isRequired,
|
||||||
|
onSearchAllMissingPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Missing;
|
173
frontend/src/Wanted/Missing/MissingConnector.js
Normal file
173
frontend/src/Wanted/Missing/MissingConnector.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
|
import * as wantedActions from 'Store/Actions/wantedActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import Missing from './Missing';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.wanted.missing,
|
||||||
|
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH),
|
||||||
|
(missing, isSearchingForMissingMovies) => {
|
||||||
|
return {
|
||||||
|
isSearchingForMissingMovies,
|
||||||
|
isSaving: missing.items.filter((m) => m.isSaving).length > 1,
|
||||||
|
...missing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...wantedActions,
|
||||||
|
executeCommand,
|
||||||
|
fetchQueueDetails,
|
||||||
|
clearQueueDetails
|
||||||
|
};
|
||||||
|
|
||||||
|
class MissingConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchMissing,
|
||||||
|
gotoMissingFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchMissing();
|
||||||
|
} else {
|
||||||
|
gotoMissingFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
const movieIds = selectUniqueIds(this.props.items, 'id');
|
||||||
|
this.props.fetchQueueDetails({ movieIds });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
this.props.clearMissing();
|
||||||
|
this.props.clearQueueDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchMissing();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFirstPagePress = () => {
|
||||||
|
this.props.gotoMissingFirstPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPreviousPagePress = () => {
|
||||||
|
this.props.gotoMissingPreviousPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onNextPagePress = () => {
|
||||||
|
this.props.gotoMissingNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onLastPagePress = () => {
|
||||||
|
this.props.gotoMissingLastPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPageSelect = (page) => {
|
||||||
|
this.props.gotoMissingPage({ page });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.setMissingSort({ sortKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setMissingFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setMissingTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoMissingFirstPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchSelectedPress = (selected) => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.MOVIE_SEARCH,
|
||||||
|
movieIds: selected
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllMissingPress = (monitored) => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.MISSING_MOVIES_SEARCH,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Missing
|
||||||
|
onFirstPagePress={this.onFirstPagePress}
|
||||||
|
onPreviousPagePress={this.onPreviousPagePress}
|
||||||
|
onNextPagePress={this.onNextPagePress}
|
||||||
|
onLastPagePress={this.onLastPagePress}
|
||||||
|
onPageSelect={this.onPageSelect}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onSearchSelectedPress={this.onSearchSelectedPress}
|
||||||
|
onSearchAllMissingPress={this.onSearchAllMissingPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MissingConnector.propTypes = {
|
||||||
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchMissing: PropTypes.func.isRequired,
|
||||||
|
gotoMissingFirstPage: PropTypes.func.isRequired,
|
||||||
|
gotoMissingPreviousPage: PropTypes.func.isRequired,
|
||||||
|
gotoMissingNextPage: PropTypes.func.isRequired,
|
||||||
|
gotoMissingLastPage: PropTypes.func.isRequired,
|
||||||
|
gotoMissingPage: PropTypes.func.isRequired,
|
||||||
|
setMissingSort: PropTypes.func.isRequired,
|
||||||
|
setMissingFilter: PropTypes.func.isRequired,
|
||||||
|
setMissingTableOption: PropTypes.func.isRequired,
|
||||||
|
clearMissing: PropTypes.func.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired,
|
||||||
|
fetchQueueDetails: PropTypes.func.isRequired,
|
||||||
|
clearQueueDetails: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(MissingConnector)
|
||||||
|
);
|
5
frontend/src/Wanted/Missing/MissingRow.css
Normal file
5
frontend/src/Wanted/Missing/MissingRow.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.status {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 100px;
|
||||||
|
}
|
7
frontend/src/Wanted/Missing/MissingRow.css.d.ts
vendored
Normal file
7
frontend/src/Wanted/Missing/MissingRow.css.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'status': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
110
frontend/src/Wanted/Missing/MissingRow.js
Normal file
110
frontend/src/Wanted/Missing/MissingRow.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import movieEntities from 'Movie/movieEntities';
|
||||||
|
import MovieSearchCellConnector from 'Movie/MovieSearchCellConnector';
|
||||||
|
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import styles from './MissingRow.css';
|
||||||
|
|
||||||
|
function MissingRow(props) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
movieFileId,
|
||||||
|
year,
|
||||||
|
title,
|
||||||
|
titleSlug,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieTitleLink
|
||||||
|
titleSlug={titleSlug}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.year') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{year}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.status}
|
||||||
|
>
|
||||||
|
<MovieStatusConnector
|
||||||
|
movieId={id}
|
||||||
|
movieFileId={movieFileId}
|
||||||
|
movieEntity={movieEntities.WANTED_MISSING}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<MovieSearchCellConnector
|
||||||
|
key={name}
|
||||||
|
movieId={id}
|
||||||
|
movieTitle={title}
|
||||||
|
movieEntity={movieEntities.WANTED_MISSING}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MissingRow.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
movieFileId: PropTypes.number,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
titleSlug: PropTypes.string.isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MissingRow;
|
@ -267,6 +267,8 @@
|
|||||||
"Cutoff": "Cutoff",
|
"Cutoff": "Cutoff",
|
||||||
"CutoffNotMet": "Cutoff Not Met",
|
"CutoffNotMet": "Cutoff Not Met",
|
||||||
"CutoffUnmet": "Cut-off Unmet",
|
"CutoffUnmet": "Cut-off Unmet",
|
||||||
|
"CutoffUnmetLoadError": "Error loading cutoff unmet items",
|
||||||
|
"CutoffUnmetNoItems": "No cutoff unmet items",
|
||||||
"Dash": "Dash",
|
"Dash": "Dash",
|
||||||
"Database": "Database",
|
"Database": "Database",
|
||||||
"DatabaseMigration": "Database Migration",
|
"DatabaseMigration": "Database Migration",
|
||||||
@ -783,6 +785,7 @@
|
|||||||
"InteractiveImportNoQuality": "Quality must be chosen for each selected file",
|
"InteractiveImportNoQuality": "Quality must be chosen for each selected file",
|
||||||
"InteractiveSearch": "Interactive Search",
|
"InteractiveSearch": "Interactive Search",
|
||||||
"InteractiveSearchModalHeader": "Interactive Search",
|
"InteractiveSearchModalHeader": "Interactive Search",
|
||||||
|
"InteractiveSearchModalHeaderTitle": "Interactive Search - {title}",
|
||||||
"InteractiveSearchResultsFailedErrorMessage": "Search failed because its {message}. Try refreshing the movie info and verify the necessary information is present before searching again.",
|
"InteractiveSearchResultsFailedErrorMessage": "Search failed because its {message}. Try refreshing the movie info and verify the necessary information is present before searching again.",
|
||||||
"Interval": "Interval",
|
"Interval": "Interval",
|
||||||
"InvalidFormat": "Invalid Format",
|
"InvalidFormat": "Invalid Format",
|
||||||
@ -854,6 +857,7 @@
|
|||||||
"MarkAsFailed": "Mark as Failed",
|
"MarkAsFailed": "Mark as Failed",
|
||||||
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
|
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
|
||||||
"MassMovieSearch": "Mass Movie Search",
|
"MassMovieSearch": "Mass Movie Search",
|
||||||
|
"MassSearchCancelWarning": "This cannot be cancelled once started without restarting {appName} or disabling all of your indexers.",
|
||||||
"MatchedToMovie": "Matched to Movie",
|
"MatchedToMovie": "Matched to Movie",
|
||||||
"Max": "Max",
|
"Max": "Max",
|
||||||
"MaximumLimits": "Maximum Limits",
|
"MaximumLimits": "Maximum Limits",
|
||||||
@ -889,7 +893,9 @@
|
|||||||
"MinutesNinety": "90 Minutes: {ninety}",
|
"MinutesNinety": "90 Minutes: {ninety}",
|
||||||
"MinutesSixty": "60 Minutes: {sixty}",
|
"MinutesSixty": "60 Minutes: {sixty}",
|
||||||
"Missing": "Missing",
|
"Missing": "Missing",
|
||||||
|
"MissingLoadError": "Error loading missing items",
|
||||||
"MissingMonitoredAndConsideredAvailable": "Missing (Monitored)",
|
"MissingMonitoredAndConsideredAvailable": "Missing (Monitored)",
|
||||||
|
"MissingNoItems": "No missing items",
|
||||||
"MissingNotMonitored": "Missing (Unmonitored)",
|
"MissingNotMonitored": "Missing (Unmonitored)",
|
||||||
"Mode": "Mode",
|
"Mode": "Mode",
|
||||||
"Monday": "Monday",
|
"Monday": "Monday",
|
||||||
@ -897,6 +903,7 @@
|
|||||||
"MonitorCollection": "Monitor Collection",
|
"MonitorCollection": "Monitor Collection",
|
||||||
"MonitorMovie": "Monitor Movie",
|
"MonitorMovie": "Monitor Movie",
|
||||||
"MonitorMovies": "Monitor Movies",
|
"MonitorMovies": "Monitor Movies",
|
||||||
|
"MonitorSelected": "Monitor Selected",
|
||||||
"Monitored": "Monitored",
|
"Monitored": "Monitored",
|
||||||
"MonitoredCollectionHelpText": "Monitor to automatically have movies from this collection added to the library",
|
"MonitoredCollectionHelpText": "Monitor to automatically have movies from this collection added to the library",
|
||||||
"MonitoredHelpText": "Download movie if available",
|
"MonitoredHelpText": "Download movie if available",
|
||||||
@ -922,6 +929,7 @@
|
|||||||
"MovieDetailsPreviousMovie": "Movie Details: Previous Movie",
|
"MovieDetailsPreviousMovie": "Movie Details: Previous Movie",
|
||||||
"MovieDownloadFailedTooltip": "Movie download failed",
|
"MovieDownloadFailedTooltip": "Movie download failed",
|
||||||
"MovieDownloadIgnoredTooltip": "Movie Download Ignored",
|
"MovieDownloadIgnoredTooltip": "Movie Download Ignored",
|
||||||
|
"MovieDownloaded": "Movie Downloaded",
|
||||||
"MovieEditor": "Movie Editor",
|
"MovieEditor": "Movie Editor",
|
||||||
"MovieExcludedFromAutomaticAdd": "Movie Excluded From Automatic Add",
|
"MovieExcludedFromAutomaticAdd": "Movie Excluded From Automatic Add",
|
||||||
"MovieFileDeleted": "Movie File Deleted",
|
"MovieFileDeleted": "Movie File Deleted",
|
||||||
@ -948,12 +956,15 @@
|
|||||||
"MovieInvalidFormat": "Movie: Invalid Format",
|
"MovieInvalidFormat": "Movie: Invalid Format",
|
||||||
"MovieIsDownloading": "Movie is downloading",
|
"MovieIsDownloading": "Movie is downloading",
|
||||||
"MovieIsMonitored": "Movie is monitored",
|
"MovieIsMonitored": "Movie is monitored",
|
||||||
|
"MovieIsNotAvailable": "Movie is not available",
|
||||||
|
"MovieIsNotMonitored": "Movie is not monitored",
|
||||||
"MovieIsOnImportExclusionList": "Movie is on Import Exclusion List",
|
"MovieIsOnImportExclusionList": "Movie is on Import Exclusion List",
|
||||||
"MovieIsPopular": "Movie is Popular on TMDb",
|
"MovieIsPopular": "Movie is Popular on TMDb",
|
||||||
"MovieIsRecommend": "Movie is recommended based on recent addition",
|
"MovieIsRecommend": "Movie is recommended based on recent addition",
|
||||||
"MovieIsTrending": "Movie is Trending on TMDb",
|
"MovieIsTrending": "Movie is Trending on TMDb",
|
||||||
"MovieIsUnmonitored": "Movie is unmonitored",
|
"MovieIsUnmonitored": "Movie is unmonitored",
|
||||||
"MovieMatchType": "Movie Match Type",
|
"MovieMatchType": "Movie Match Type",
|
||||||
|
"MovieMissingFromDisk": "Movie missing from disk",
|
||||||
"MovieNaming": "Movie Naming",
|
"MovieNaming": "Movie Naming",
|
||||||
"MovieOnly": "Movie Only",
|
"MovieOnly": "Movie Only",
|
||||||
"MovieSearchResultsLoadError": "Unable to load results for this movie search. Try again later",
|
"MovieSearchResultsLoadError": "Unable to load results for this movie search. Try again later",
|
||||||
@ -1490,6 +1501,10 @@
|
|||||||
"SearchCutoffUnmet": "Search Cutoff Unmet",
|
"SearchCutoffUnmet": "Search Cutoff Unmet",
|
||||||
"SearchFailedPleaseTryAgainLater": "Search failed, please try again later.",
|
"SearchFailedPleaseTryAgainLater": "Search failed, please try again later.",
|
||||||
"SearchFiltered": "Search Filtered",
|
"SearchFiltered": "Search Filtered",
|
||||||
|
"SearchForAllMissingMovies": "Search for all missing movies",
|
||||||
|
"SearchForAllMissingMoviesConfirmationCount": "Are you sure you want to search for all {totalRecords} missing movies?",
|
||||||
|
"SearchForCutoffUnmetMovies": "Search for all Cutoff Unmet movies",
|
||||||
|
"SearchForCutoffUnmetMoviesConfirmationCount": "Are you sure you want to search for all {totalRecords} Cutoff Unmet movies?",
|
||||||
"SearchForMissing": "Search for Missing",
|
"SearchForMissing": "Search for Missing",
|
||||||
"SearchForMovie": "Search for movie",
|
"SearchForMovie": "Search for movie",
|
||||||
"SearchIsNotSupportedWithThisIndexer": "Search is not supported with this indexer",
|
"SearchIsNotSupportedWithThisIndexer": "Search is not supported with this indexer",
|
||||||
@ -1700,6 +1715,7 @@
|
|||||||
"Unlimited": "Unlimited",
|
"Unlimited": "Unlimited",
|
||||||
"UnmappedFilesOnly": "Unmapped Files Only",
|
"UnmappedFilesOnly": "Unmapped Files Only",
|
||||||
"UnmappedFolders": "Unmapped Folders",
|
"UnmappedFolders": "Unmapped Folders",
|
||||||
|
"UnmonitorSelected": "Unmonitor Selected",
|
||||||
"Unmonitored": "Unmonitored",
|
"Unmonitored": "Unmonitored",
|
||||||
"Unreleased": "Unreleased",
|
"Unreleased": "Unreleased",
|
||||||
"UnsavedChanges": "Unsaved Changes",
|
"UnsavedChanges": "Unsaved Changes",
|
||||||
|
@ -48,6 +48,13 @@ public MovieRepository(IMainDatabase database,
|
|||||||
_alternativeTitleRepository = alternativeTitleRepository;
|
_alternativeTitleRepository = alternativeTitleRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<Movie> PagedQuery(SqlBuilder builder) =>
|
||||||
|
_database.QueryJoined<Movie, MovieMetadata>(builder, (movie, movieMetadata) =>
|
||||||
|
{
|
||||||
|
movie.MovieMetadata = movieMetadata;
|
||||||
|
return movie;
|
||||||
|
});
|
||||||
|
|
||||||
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
||||||
.Join<Movie, QualityProfile>((m, p) => m.QualityProfileId == p.Id)
|
.Join<Movie, QualityProfile>((m, p) => m.QualityProfileId == p.Id)
|
||||||
.Join<Movie, MovieMetadata>((m, p) => m.MovieMetadataId == p.Id)
|
.Join<Movie, MovieMetadata>((m, p) => m.MovieMetadataId == p.Id)
|
||||||
@ -242,24 +249,26 @@ public List<Movie> MoviesBetweenDates(DateTime start, DateTime end, bool include
|
|||||||
}
|
}
|
||||||
|
|
||||||
public SqlBuilder MoviesWithoutFilesBuilder() => Builder()
|
public SqlBuilder MoviesWithoutFilesBuilder() => Builder()
|
||||||
.Where<Movie>(x => x.MovieFileId == 0);
|
.Where<Movie>(x => x.MovieFileId == 0)
|
||||||
|
.GroupBy<Movie>(e => e.Id);
|
||||||
|
|
||||||
public PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec)
|
public PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec)
|
||||||
{
|
{
|
||||||
pagingSpec.Records = GetPagedRecords(MoviesWithoutFilesBuilder(), pagingSpec, PagedQuery);
|
pagingSpec.Records = GetPagedRecords(MoviesWithoutFilesBuilder(), pagingSpec, PagedQuery);
|
||||||
pagingSpec.TotalRecords = GetPagedRecordCount(MoviesWithoutFilesBuilder().SelectCount(), pagingSpec);
|
pagingSpec.TotalRecords = GetPagedRecordCount(MoviesWithoutFilesBuilder().SelectCountDistinct<Movie>(x => x.Id), pagingSpec);
|
||||||
|
|
||||||
return pagingSpec;
|
return pagingSpec;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SqlBuilder MoviesWhereCutoffUnmetBuilder(List<QualitiesBelowCutoff> qualitiesBelowCutoff) => Builder()
|
public SqlBuilder MoviesWhereCutoffUnmetBuilder(List<QualitiesBelowCutoff> qualitiesBelowCutoff) => Builder()
|
||||||
.Where<Movie>(x => x.MovieFileId != 0)
|
.Where<Movie>(x => x.MovieFileId != 0)
|
||||||
.Where(BuildQualityCutoffWhereClause(qualitiesBelowCutoff));
|
.Where(BuildQualityCutoffWhereClause(qualitiesBelowCutoff))
|
||||||
|
.GroupBy<Movie>(e => e.Id);
|
||||||
|
|
||||||
public PagingSpec<Movie> MoviesWhereCutoffUnmet(PagingSpec<Movie> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff)
|
public PagingSpec<Movie> MoviesWhereCutoffUnmet(PagingSpec<Movie> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff)
|
||||||
{
|
{
|
||||||
pagingSpec.Records = GetPagedRecords(MoviesWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery);
|
pagingSpec.Records = GetPagedRecords(MoviesWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery);
|
||||||
pagingSpec.TotalRecords = GetPagedRecordCount(MoviesWhereCutoffUnmetBuilder(qualitiesBelowCutoff).SelectCount(), pagingSpec);
|
pagingSpec.TotalRecords = GetPagedRecordCount(MoviesWhereCutoffUnmetBuilder(qualitiesBelowCutoff).SelectCountDistinct<Movie>(x => x.Id), pagingSpec);
|
||||||
|
|
||||||
return pagingSpec;
|
return pagingSpec;
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.Languages;
|
|
||||||
using NzbDrone.Core.Movies;
|
using NzbDrone.Core.Movies;
|
||||||
using NzbDrone.Core.Movies.Translations;
|
using NzbDrone.Core.Movies.Translations;
|
||||||
using NzbDrone.Core.MovieStats;
|
using NzbDrone.Core.MovieStats;
|
||||||
@ -13,35 +13,27 @@
|
|||||||
using NzbDrone.SignalR;
|
using NzbDrone.SignalR;
|
||||||
using Radarr.Api.V3.Movies;
|
using Radarr.Api.V3.Movies;
|
||||||
using Radarr.Http;
|
using Radarr.Http;
|
||||||
using Radarr.Http.REST;
|
|
||||||
|
|
||||||
namespace Radarr.Api.V3.Calendar
|
namespace Radarr.Api.V3.Calendar
|
||||||
{
|
{
|
||||||
[V3ApiController]
|
[V3ApiController]
|
||||||
public class CalendarController : RestControllerWithSignalR<MovieResource, Movie>
|
public class CalendarController : MovieControllerWithSignalR
|
||||||
{
|
{
|
||||||
private readonly IMovieService _moviesService;
|
private readonly IMovieService _moviesService;
|
||||||
private readonly IMovieTranslationService _movieTranslationService;
|
|
||||||
private readonly IMovieStatisticsService _movieStatisticsService;
|
|
||||||
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
|
|
||||||
private readonly ITagService _tagService;
|
private readonly ITagService _tagService;
|
||||||
private readonly IConfigService _configService;
|
|
||||||
|
|
||||||
public CalendarController(IBroadcastSignalRMessage signalR,
|
public CalendarController(IBroadcastSignalRMessage signalR,
|
||||||
IMovieService moviesService,
|
IMovieService movieService,
|
||||||
IMovieTranslationService movieTranslationService,
|
IMovieTranslationService movieTranslationService,
|
||||||
IMovieStatisticsService movieStatisticsService,
|
IMovieStatisticsService movieStatisticsService,
|
||||||
IUpgradableSpecification qualityUpgradableSpecification,
|
IUpgradableSpecification upgradableSpecification,
|
||||||
|
ICustomFormatCalculationService formatCalculator,
|
||||||
ITagService tagService,
|
ITagService tagService,
|
||||||
IConfigService configService)
|
IConfigService configService)
|
||||||
: base(signalR)
|
: base(movieService, movieTranslationService, movieStatisticsService, upgradableSpecification, formatCalculator, configService, signalR)
|
||||||
{
|
{
|
||||||
_moviesService = moviesService;
|
_moviesService = movieService;
|
||||||
_movieTranslationService = movieTranslationService;
|
|
||||||
_movieStatisticsService = movieStatisticsService;
|
|
||||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
|
||||||
_tagService = tagService;
|
_tagService = tagService;
|
||||||
_configService = configService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[NonAction]
|
[NonAction]
|
||||||
@ -84,56 +76,5 @@ public List<MovieResource> GetCalendar(DateTime? start, DateTime? end, bool unmo
|
|||||||
|
|
||||||
return resources.OrderBy(e => e.InCinemas).ToList();
|
return resources.OrderBy(e => e.InCinemas).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<MovieResource> MapToResource(List<Movie> movies)
|
|
||||||
{
|
|
||||||
var resources = new List<MovieResource>();
|
|
||||||
var availDelay = _configService.AvailabilityDelay;
|
|
||||||
var language = (Language)_configService.MovieInfoLanguage;
|
|
||||||
|
|
||||||
foreach (var movie in movies)
|
|
||||||
{
|
|
||||||
if (movie == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId);
|
|
||||||
var translation = GetMovieTranslation(translations, movie.MovieMetadata, language);
|
|
||||||
|
|
||||||
var resource = movie.ToResource(availDelay, translation, _qualityUpgradableSpecification);
|
|
||||||
FetchAndLinkMovieStatistics(resource);
|
|
||||||
|
|
||||||
resources.Add(resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resources;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MovieTranslation GetMovieTranslation(List<MovieTranslation> translations, MovieMetadata movie, Language language)
|
|
||||||
{
|
|
||||||
if (language == Language.Original)
|
|
||||||
{
|
|
||||||
return new MovieTranslation
|
|
||||||
{
|
|
||||||
Title = movie.OriginalTitle,
|
|
||||||
Overview = movie.Overview
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return translations.FirstOrDefault(t => t.Language == language && t.MovieMetadataId == movie.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FetchAndLinkMovieStatistics(MovieResource resource)
|
|
||||||
{
|
|
||||||
LinkMovieStatistics(resource, _movieStatisticsService.MovieStatistics(resource.Id));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LinkMovieStatistics(MovieResource resource, MovieStatistics movieStatistics)
|
|
||||||
{
|
|
||||||
resource.Statistics = movieStatistics.ToResource();
|
|
||||||
resource.HasFile = movieStatistics.MovieFileCount > 0;
|
|
||||||
resource.SizeOnDisk = movieStatistics.SizeOnDisk;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
159
src/Radarr.Api.V3/Movies/MovieControllerWithSignalR.cs
Normal file
159
src/Radarr.Api.V3/Movies/MovieControllerWithSignalR.cs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
|
using NzbDrone.Core.Datastore.Events;
|
||||||
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.MediaFiles.Events;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Movies.Translations;
|
||||||
|
using NzbDrone.Core.MovieStats;
|
||||||
|
using NzbDrone.SignalR;
|
||||||
|
using Radarr.Http.REST;
|
||||||
|
|
||||||
|
namespace Radarr.Api.V3.Movies
|
||||||
|
{
|
||||||
|
public abstract class MovieControllerWithSignalR : RestControllerWithSignalR<MovieResource, Movie>,
|
||||||
|
IHandle<MovieGrabbedEvent>,
|
||||||
|
IHandle<MovieFileImportedEvent>,
|
||||||
|
IHandle<MovieFileDeletedEvent>
|
||||||
|
{
|
||||||
|
protected readonly IMovieService _movieService;
|
||||||
|
protected readonly IMovieTranslationService _movieTranslationService;
|
||||||
|
protected readonly IMovieStatisticsService _movieStatisticsService;
|
||||||
|
protected readonly IUpgradableSpecification _upgradableSpecification;
|
||||||
|
protected readonly ICustomFormatCalculationService _formatCalculator;
|
||||||
|
protected readonly IConfigService _configService;
|
||||||
|
|
||||||
|
protected MovieControllerWithSignalR(IMovieService movieService,
|
||||||
|
IMovieTranslationService movieTranslationService,
|
||||||
|
IMovieStatisticsService movieStatisticsService,
|
||||||
|
IUpgradableSpecification upgradableSpecification,
|
||||||
|
ICustomFormatCalculationService formatCalculator,
|
||||||
|
IConfigService configService,
|
||||||
|
IBroadcastSignalRMessage signalRBroadcaster)
|
||||||
|
: base(signalRBroadcaster)
|
||||||
|
{
|
||||||
|
_movieService = movieService;
|
||||||
|
_movieTranslationService = movieTranslationService;
|
||||||
|
_movieStatisticsService = movieStatisticsService;
|
||||||
|
_upgradableSpecification = upgradableSpecification;
|
||||||
|
_formatCalculator = formatCalculator;
|
||||||
|
_configService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected MovieControllerWithSignalR(IMovieService movieService,
|
||||||
|
IUpgradableSpecification upgradableSpecification,
|
||||||
|
ICustomFormatCalculationService formatCalculator,
|
||||||
|
IBroadcastSignalRMessage signalRBroadcaster,
|
||||||
|
string resource)
|
||||||
|
: base(signalRBroadcaster)
|
||||||
|
{
|
||||||
|
_movieService = movieService;
|
||||||
|
_upgradableSpecification = upgradableSpecification;
|
||||||
|
_formatCalculator = formatCalculator;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MovieResource GetResourceById(int id)
|
||||||
|
{
|
||||||
|
var movie = _movieService.GetMovie(id);
|
||||||
|
var resource = MapToResource(movie);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected MovieResource MapToResource(Movie movie)
|
||||||
|
{
|
||||||
|
if (movie == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var availDelay = _configService.AvailabilityDelay;
|
||||||
|
var language = (Language)_configService.MovieInfoLanguage;
|
||||||
|
|
||||||
|
var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId);
|
||||||
|
var translation = GetMovieTranslation(translations, movie.MovieMetadata, language);
|
||||||
|
|
||||||
|
var resource = movie.ToResource(availDelay, translation, _upgradableSpecification, _formatCalculator);
|
||||||
|
FetchAndLinkMovieStatistics(resource);
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<MovieResource> MapToResource(List<Movie> movies)
|
||||||
|
{
|
||||||
|
var resources = new List<MovieResource>();
|
||||||
|
var availDelay = _configService.AvailabilityDelay;
|
||||||
|
var language = (Language)_configService.MovieInfoLanguage;
|
||||||
|
|
||||||
|
foreach (var movie in movies)
|
||||||
|
{
|
||||||
|
if (movie == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId);
|
||||||
|
var translation = GetMovieTranslation(translations, movie.MovieMetadata, language);
|
||||||
|
|
||||||
|
var resource = movie.ToResource(availDelay, translation, _upgradableSpecification, _formatCalculator);
|
||||||
|
FetchAndLinkMovieStatistics(resource);
|
||||||
|
|
||||||
|
resources.Add(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MovieTranslation GetMovieTranslation(List<MovieTranslation> translations, MovieMetadata movie, Language language)
|
||||||
|
{
|
||||||
|
if (language == Language.Original)
|
||||||
|
{
|
||||||
|
return new MovieTranslation
|
||||||
|
{
|
||||||
|
Title = movie.OriginalTitle,
|
||||||
|
Overview = movie.Overview
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return translations.FirstOrDefault(t => t.Language == language && t.MovieMetadataId == movie.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FetchAndLinkMovieStatistics(MovieResource resource)
|
||||||
|
{
|
||||||
|
LinkMovieStatistics(resource, _movieStatisticsService.MovieStatistics(resource.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LinkMovieStatistics(MovieResource resource, MovieStatistics movieStatistics)
|
||||||
|
{
|
||||||
|
resource.Statistics = movieStatistics.ToResource();
|
||||||
|
resource.HasFile = movieStatistics.MovieFileCount > 0;
|
||||||
|
resource.SizeOnDisk = movieStatistics.SizeOnDisk;
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
public void Handle(MovieGrabbedEvent message)
|
||||||
|
{
|
||||||
|
var resource = message.Movie.Movie.ToResource(0, null, _upgradableSpecification, _formatCalculator);
|
||||||
|
resource.Grabbed = true;
|
||||||
|
|
||||||
|
BroadcastResourceChange(ModelAction.Updated, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
public void Handle(MovieFileImportedEvent message)
|
||||||
|
{
|
||||||
|
BroadcastResourceChange(ModelAction.Updated, message.MovieInfo.Movie.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
public void Handle(MovieFileDeletedEvent message)
|
||||||
|
{
|
||||||
|
BroadcastResourceChange(ModelAction.Updated, message.MovieFile.Movie.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
@ -56,6 +57,7 @@ public MovieResource()
|
|||||||
|
|
||||||
// Compatibility
|
// Compatibility
|
||||||
public bool? HasFile { get; set; }
|
public bool? HasFile { get; set; }
|
||||||
|
public int MovieFileId { get; set; }
|
||||||
|
|
||||||
// Editing Only
|
// Editing Only
|
||||||
public bool Monitored { get; set; }
|
public bool Monitored { get; set; }
|
||||||
@ -80,6 +82,10 @@ public MovieResource()
|
|||||||
public MovieCollectionResource Collection { get; set; }
|
public MovieCollectionResource Collection { get; set; }
|
||||||
public float Popularity { get; set; }
|
public float Popularity { get; set; }
|
||||||
public MovieStatisticsResource Statistics { get; set; }
|
public MovieStatisticsResource Statistics { get; set; }
|
||||||
|
|
||||||
|
// Hiding this so people don't think its usable (only used to set the initial state)
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
public bool Grabbed { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class MovieResourceMapper
|
public static class MovieResourceMapper
|
||||||
@ -118,6 +124,8 @@ public static MovieResource ToResource(this Movie model, int availDelay, MovieTr
|
|||||||
Year = model.Year,
|
Year = model.Year,
|
||||||
SecondaryYear = model.MovieMetadata.Value.SecondaryYear,
|
SecondaryYear = model.MovieMetadata.Value.SecondaryYear,
|
||||||
|
|
||||||
|
MovieFileId = model.MovieFileId,
|
||||||
|
|
||||||
Path = model.Path,
|
Path = model.Path,
|
||||||
QualityProfileId = model.QualityProfileId,
|
QualityProfileId = model.QualityProfileId,
|
||||||
|
|
||||||
|
61
src/Radarr.Api.V3/Wanted/CutoffController.cs
Normal file
61
src/Radarr.Api.V3/Wanted/CutoffController.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Movies.Translations;
|
||||||
|
using NzbDrone.Core.MovieStats;
|
||||||
|
using NzbDrone.SignalR;
|
||||||
|
using Radarr.Api.V3.Movies;
|
||||||
|
using Radarr.Http;
|
||||||
|
using Radarr.Http.Extensions;
|
||||||
|
|
||||||
|
namespace Radarr.Api.V3.Wanted
|
||||||
|
{
|
||||||
|
[V3ApiController("wanted/cutoff")]
|
||||||
|
public class CutoffController : MovieControllerWithSignalR
|
||||||
|
{
|
||||||
|
private readonly IMovieCutoffService _movieCutoffService;
|
||||||
|
|
||||||
|
public CutoffController(IMovieCutoffService movieCutoffService,
|
||||||
|
IMovieService movieService,
|
||||||
|
IMovieTranslationService movieTranslationService,
|
||||||
|
IMovieStatisticsService movieStatisticsService,
|
||||||
|
IUpgradableSpecification upgradableSpecification,
|
||||||
|
ICustomFormatCalculationService formatCalculator,
|
||||||
|
IConfigService configService,
|
||||||
|
IBroadcastSignalRMessage signalRBroadcaster)
|
||||||
|
: base(movieService, movieTranslationService, movieStatisticsService, upgradableSpecification, formatCalculator, configService, signalRBroadcaster)
|
||||||
|
{
|
||||||
|
_movieCutoffService = movieCutoffService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
protected override MovieResource GetResourceById(int id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public PagingResource<MovieResource> GetCutoffUnmetMovies([FromQuery] PagingRequestResource paging, bool monitored = true)
|
||||||
|
{
|
||||||
|
var pagingResource = new PagingResource<MovieResource>(paging);
|
||||||
|
var pagingSpec = new PagingSpec<Movie>
|
||||||
|
{
|
||||||
|
Page = pagingResource.Page,
|
||||||
|
PageSize = pagingResource.PageSize,
|
||||||
|
SortKey = pagingResource.SortKey,
|
||||||
|
SortDirection = pagingResource.SortDirection
|
||||||
|
};
|
||||||
|
|
||||||
|
pagingSpec.FilterExpressions.Add(v => v.Monitored == monitored);
|
||||||
|
|
||||||
|
var resource = pagingSpec.ApplyToPage(_movieCutoffService.MoviesWhereCutoffUnmet, v => MapToResource(v));
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
src/Radarr.Api.V3/Wanted/MissingController.cs
Normal file
57
src/Radarr.Api.V3/Wanted/MissingController.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Movies.Translations;
|
||||||
|
using NzbDrone.Core.MovieStats;
|
||||||
|
using NzbDrone.SignalR;
|
||||||
|
using Radarr.Api.V3.Movies;
|
||||||
|
using Radarr.Http;
|
||||||
|
using Radarr.Http.Extensions;
|
||||||
|
|
||||||
|
namespace Radarr.Api.V3.Wanted
|
||||||
|
{
|
||||||
|
[V3ApiController("wanted/missing")]
|
||||||
|
public class MissingController : MovieControllerWithSignalR
|
||||||
|
{
|
||||||
|
public MissingController(IMovieService movieService,
|
||||||
|
IMovieTranslationService movieTranslationService,
|
||||||
|
IMovieStatisticsService movieStatisticsService,
|
||||||
|
IUpgradableSpecification upgradableSpecification,
|
||||||
|
ICustomFormatCalculationService formatCalculator,
|
||||||
|
IConfigService configService,
|
||||||
|
IBroadcastSignalRMessage signalRBroadcaster)
|
||||||
|
: base(movieService, movieTranslationService, movieStatisticsService, upgradableSpecification, formatCalculator, configService, signalRBroadcaster)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
protected override MovieResource GetResourceById(int id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public PagingResource<MovieResource> GetMissingMovies([FromQuery] PagingRequestResource paging, bool monitored = true)
|
||||||
|
{
|
||||||
|
var pagingResource = new PagingResource<MovieResource>(paging);
|
||||||
|
var pagingSpec = new PagingSpec<Movie>
|
||||||
|
{
|
||||||
|
Page = pagingResource.Page,
|
||||||
|
PageSize = pagingResource.PageSize,
|
||||||
|
SortKey = pagingResource.SortKey,
|
||||||
|
SortDirection = pagingResource.SortDirection
|
||||||
|
};
|
||||||
|
|
||||||
|
pagingSpec.FilterExpressions.Add(v => v.Monitored == monitored);
|
||||||
|
|
||||||
|
var resource = pagingSpec.ApplyToPage(_movieService.MoviesWithoutFiles, v => MapToResource(v));
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user