mirror of
https://github.com/Radarr/Radarr
synced 2025-03-14 16:09:37 +00:00
New: Wanted Cutoff/Missing
This commit is contained in:
parent
9798202589
commit
152f50a1ef
37 changed files with 2267 additions and 88 deletions
|
@ -33,6 +33,8 @@ import Status from 'System/Status/Status';
|
|||
import Tasks from 'System/Tasks/Tasks';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||
|
||||
function AppRoutes(props) {
|
||||
const {
|
||||
|
@ -121,6 +123,20 @@ function AppRoutes(props) {
|
|||
component={BlocklistConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Wanted
|
||||
*/}
|
||||
|
||||
<Route
|
||||
path="/wanted/missing"
|
||||
component={MissingConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/wanted/cutoffunmet"
|
||||
component={CutoffUnmetConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
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,
|
||||
title: () => translate('Settings'),
|
||||
|
|
|
@ -244,6 +244,26 @@ class SignalRConnector extends Component {
|
|||
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 = () => {
|
||||
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 {
|
||||
isOpen,
|
||||
movieId,
|
||||
movieTitle,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
|
@ -20,6 +21,7 @@ function MovieInteractiveSearchModal(props) {
|
|||
>
|
||||
<MovieInteractiveSearchModalContent
|
||||
movieId={movieId}
|
||||
movieTitle={movieTitle}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
|
@ -29,6 +31,7 @@ function MovieInteractiveSearchModal(props) {
|
|||
MovieInteractiveSearchModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
movieId: PropTypes.number.isRequired,
|
||||
movieTitle: PropTypes.string,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -12,13 +12,17 @@ import translate from 'Utilities/String/translate';
|
|||
function MovieInteractiveSearchModalContent(props) {
|
||||
const {
|
||||
movieId,
|
||||
movieTitle,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('InteractiveSearchModalHeader')}
|
||||
{movieTitle === undefined ?
|
||||
translate('InteractiveSearchModalHeader') :
|
||||
translate('InteractiveSearchModalHeaderTitle', { title: movieTitle })
|
||||
}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
|
@ -38,6 +42,7 @@ function MovieInteractiveSearchModalContent(props) {
|
|||
|
||||
MovieInteractiveSearchModalContent.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
movieTitle: PropTypes.string,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
export const CALENDAR = 'calendar';
|
||||
export const MOVIES = 'movies';
|
||||
export const INTERACTIVE_IMPORT = 'interactiveImport.movies';
|
||||
export const WANTED_CUTOFF_UNMET = 'wanted.cutoffUnmet';
|
||||
export const WANTED_MISSING = 'wanted.missing';
|
||||
|
||||
export default {
|
||||
CALENDAR,
|
||||
MOVIES,
|
||||
INTERACTIVE_IMPORT
|
||||
INTERACTIVE_IMPORT,
|
||||
WANTED_CUTOFF_UNMET,
|
||||
WANTED_MISSING
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@ function createMapStateToProps() {
|
|||
createMovieFileSelector(),
|
||||
(movieFile) => {
|
||||
return {
|
||||
language: movieFile ? movieFile.language : undefined
|
||||
languages: movieFile ? movieFile.languages : undefined
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import updateEpisodes from 'Utilities/Episode/updateEpisodes';
|
||||
import updateMovies from 'Utilities/Movie/updateMovies';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
|
||||
function createBatchToggleEpisodeMonitoredHandler(section, fetchHandler) {
|
||||
function createBatchToggleMovieMonitoredHandler(section, fetchHandler) {
|
||||
return function(getState, payload, dispatch) {
|
||||
const {
|
||||
episodeIds,
|
||||
movieIds,
|
||||
monitored
|
||||
} = payload;
|
||||
|
||||
const state = getSectionState(getState(), section, true);
|
||||
|
||||
dispatch(updateEpisodes(section, state.items, episodeIds, {
|
||||
dispatch(updateMovies(section, state.items, movieIds, {
|
||||
isSaving: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/episode/monitor',
|
||||
url: '/movie/editor',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({ episodeIds, monitored }),
|
||||
data: JSON.stringify({ movieIds, monitored }),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
dispatch(updateEpisodes(section, state.items, episodeIds, {
|
||||
dispatch(updateMovies(section, state.items, movieIds, {
|
||||
isSaving: false,
|
||||
monitored
|
||||
}));
|
||||
|
@ -32,11 +32,11 @@ function createBatchToggleEpisodeMonitoredHandler(section, fetchHandler) {
|
|||
});
|
||||
|
||||
promise.fail(() => {
|
||||
dispatch(updateEpisodes(section, state.items, episodeIds, {
|
||||
dispatch(updateMovies(section, state.items, movieIds, {
|
||||
isSaving: false
|
||||
}));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default createBatchToggleEpisodeMonitoredHandler;
|
||||
export default createBatchToggleMovieMonitoredHandler;
|
|
@ -28,6 +28,7 @@ import * as rootFolders from './rootFolderActions';
|
|||
import * as settings from './settingsActions';
|
||||
import * as system from './systemActions';
|
||||
import * as tags from './tagActions';
|
||||
import * as wanted from './wantedActions';
|
||||
|
||||
export default [
|
||||
addMovie,
|
||||
|
@ -59,5 +60,6 @@ export default [
|
|||
movieCredits,
|
||||
settings,
|
||||
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 movieEntities from 'Movie/movieEntities';
|
||||
|
||||
export function createMovieSelectorForHook(movieId) {
|
||||
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() {
|
||||
return createSelector(
|
||||
(state, { movieId }) => movieId,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
import { update } from 'Store/Actions/baseActions';
|
||||
|
||||
function updateEpisodes(section, episodes, episodeIds, options) {
|
||||
const data = _.reduce(episodes, (result, item) => {
|
||||
if (episodeIds.indexOf(item.id) > -1) {
|
||||
function updateMovies(section, movies, movieIds, options) {
|
||||
const data = _.reduce(movies, (result, item) => {
|
||||
if (movieIds.indexOf(item.id) > -1) {
|
||||
result.push({
|
||||
...item,
|
||||
...options
|
||||
|
@ -18,4 +18,4 @@ function updateEpisodes(section, episodes, episodeIds, options) {
|
|||
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",
|
||||
"CutoffNotMet": "Cutoff Not Met",
|
||||
"CutoffUnmet": "Cut-off Unmet",
|
||||
"CutoffUnmetLoadError": "Error loading cutoff unmet items",
|
||||
"CutoffUnmetNoItems": "No cutoff unmet items",
|
||||
"Dash": "Dash",
|
||||
"Database": "Database",
|
||||
"DatabaseMigration": "Database Migration",
|
||||
|
@ -783,6 +785,7 @@
|
|||
"InteractiveImportNoQuality": "Quality must be chosen for each selected file",
|
||||
"InteractiveSearch": "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.",
|
||||
"Interval": "Interval",
|
||||
"InvalidFormat": "Invalid Format",
|
||||
|
@ -854,6 +857,7 @@
|
|||
"MarkAsFailed": "Mark as Failed",
|
||||
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
|
||||
"MassMovieSearch": "Mass Movie Search",
|
||||
"MassSearchCancelWarning": "This cannot be cancelled once started without restarting {appName} or disabling all of your indexers.",
|
||||
"MatchedToMovie": "Matched to Movie",
|
||||
"Max": "Max",
|
||||
"MaximumLimits": "Maximum Limits",
|
||||
|
@ -889,7 +893,9 @@
|
|||
"MinutesNinety": "90 Minutes: {ninety}",
|
||||
"MinutesSixty": "60 Minutes: {sixty}",
|
||||
"Missing": "Missing",
|
||||
"MissingLoadError": "Error loading missing items",
|
||||
"MissingMonitoredAndConsideredAvailable": "Missing (Monitored)",
|
||||
"MissingNoItems": "No missing items",
|
||||
"MissingNotMonitored": "Missing (Unmonitored)",
|
||||
"Mode": "Mode",
|
||||
"Monday": "Monday",
|
||||
|
@ -897,6 +903,7 @@
|
|||
"MonitorCollection": "Monitor Collection",
|
||||
"MonitorMovie": "Monitor Movie",
|
||||
"MonitorMovies": "Monitor Movies",
|
||||
"MonitorSelected": "Monitor Selected",
|
||||
"Monitored": "Monitored",
|
||||
"MonitoredCollectionHelpText": "Monitor to automatically have movies from this collection added to the library",
|
||||
"MonitoredHelpText": "Download movie if available",
|
||||
|
@ -922,6 +929,7 @@
|
|||
"MovieDetailsPreviousMovie": "Movie Details: Previous Movie",
|
||||
"MovieDownloadFailedTooltip": "Movie download failed",
|
||||
"MovieDownloadIgnoredTooltip": "Movie Download Ignored",
|
||||
"MovieDownloaded": "Movie Downloaded",
|
||||
"MovieEditor": "Movie Editor",
|
||||
"MovieExcludedFromAutomaticAdd": "Movie Excluded From Automatic Add",
|
||||
"MovieFileDeleted": "Movie File Deleted",
|
||||
|
@ -948,12 +956,15 @@
|
|||
"MovieInvalidFormat": "Movie: Invalid Format",
|
||||
"MovieIsDownloading": "Movie is downloading",
|
||||
"MovieIsMonitored": "Movie is monitored",
|
||||
"MovieIsNotAvailable": "Movie is not available",
|
||||
"MovieIsNotMonitored": "Movie is not monitored",
|
||||
"MovieIsOnImportExclusionList": "Movie is on Import Exclusion List",
|
||||
"MovieIsPopular": "Movie is Popular on TMDb",
|
||||
"MovieIsRecommend": "Movie is recommended based on recent addition",
|
||||
"MovieIsTrending": "Movie is Trending on TMDb",
|
||||
"MovieIsUnmonitored": "Movie is unmonitored",
|
||||
"MovieMatchType": "Movie Match Type",
|
||||
"MovieMissingFromDisk": "Movie missing from disk",
|
||||
"MovieNaming": "Movie Naming",
|
||||
"MovieOnly": "Movie Only",
|
||||
"MovieSearchResultsLoadError": "Unable to load results for this movie search. Try again later",
|
||||
|
@ -1490,6 +1501,10 @@
|
|||
"SearchCutoffUnmet": "Search Cutoff Unmet",
|
||||
"SearchFailedPleaseTryAgainLater": "Search failed, please try again later.",
|
||||
"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",
|
||||
"SearchForMovie": "Search for movie",
|
||||
"SearchIsNotSupportedWithThisIndexer": "Search is not supported with this indexer",
|
||||
|
@ -1700,6 +1715,7 @@
|
|||
"Unlimited": "Unlimited",
|
||||
"UnmappedFilesOnly": "Unmapped Files Only",
|
||||
"UnmappedFolders": "Unmapped Folders",
|
||||
"UnmonitorSelected": "Unmonitor Selected",
|
||||
"Unmonitored": "Unmonitored",
|
||||
"Unreleased": "Unreleased",
|
||||
"UnsavedChanges": "Unsaved Changes",
|
||||
|
|
|
@ -48,6 +48,13 @@ namespace NzbDrone.Core.Movies
|
|||
_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)
|
||||
.Join<Movie, QualityProfile>((m, p) => m.QualityProfileId == p.Id)
|
||||
.Join<Movie, MovieMetadata>((m, p) => m.MovieMetadataId == p.Id)
|
||||
|
@ -242,24 +249,26 @@ namespace NzbDrone.Core.Movies
|
|||
}
|
||||
|
||||
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)
|
||||
{
|
||||
pagingSpec.Records = GetPagedRecords(MoviesWithoutFilesBuilder(), pagingSpec, PagedQuery);
|
||||
pagingSpec.TotalRecords = GetPagedRecordCount(MoviesWithoutFilesBuilder().SelectCount(), pagingSpec);
|
||||
pagingSpec.TotalRecords = GetPagedRecordCount(MoviesWithoutFilesBuilder().SelectCountDistinct<Movie>(x => x.Id), pagingSpec);
|
||||
|
||||
return pagingSpec;
|
||||
}
|
||||
|
||||
public SqlBuilder MoviesWhereCutoffUnmetBuilder(List<QualitiesBelowCutoff> qualitiesBelowCutoff) => Builder()
|
||||
.Where<Movie>(x => x.MovieFileId != 0)
|
||||
.Where(BuildQualityCutoffWhereClause(qualitiesBelowCutoff));
|
||||
.Where<Movie>(x => x.MovieFileId != 0)
|
||||
.Where(BuildQualityCutoffWhereClause(qualitiesBelowCutoff))
|
||||
.GroupBy<Movie>(e => e.Id);
|
||||
|
||||
public PagingSpec<Movie> MoviesWhereCutoffUnmet(PagingSpec<Movie> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Movies.Translations;
|
||||
using NzbDrone.Core.MovieStats;
|
||||
|
@ -13,35 +13,27 @@ using NzbDrone.Core.Tags;
|
|||
using NzbDrone.SignalR;
|
||||
using Radarr.Api.V3.Movies;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.REST;
|
||||
|
||||
namespace Radarr.Api.V3.Calendar
|
||||
{
|
||||
[V3ApiController]
|
||||
public class CalendarController : RestControllerWithSignalR<MovieResource, Movie>
|
||||
public class CalendarController : MovieControllerWithSignalR
|
||||
{
|
||||
private readonly IMovieService _moviesService;
|
||||
private readonly IMovieTranslationService _movieTranslationService;
|
||||
private readonly IMovieStatisticsService _movieStatisticsService;
|
||||
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly ITagService _tagService;
|
||||
private readonly IConfigService _configService;
|
||||
|
||||
public CalendarController(IBroadcastSignalRMessage signalR,
|
||||
IMovieService moviesService,
|
||||
IMovieService movieService,
|
||||
IMovieTranslationService movieTranslationService,
|
||||
IMovieStatisticsService movieStatisticsService,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
ITagService tagService,
|
||||
IConfigService configService)
|
||||
: base(signalR)
|
||||
: base(movieService, movieTranslationService, movieStatisticsService, upgradableSpecification, formatCalculator, configService, signalR)
|
||||
{
|
||||
_moviesService = moviesService;
|
||||
_movieTranslationService = movieTranslationService;
|
||||
_movieStatisticsService = movieStatisticsService;
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
_moviesService = movieService;
|
||||
_tagService = tagService;
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
|
@ -84,56 +76,5 @@ namespace Radarr.Api.V3.Calendar
|
|||
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
|
@ -56,6 +57,7 @@ namespace Radarr.Api.V3.Movies
|
|||
|
||||
// Compatibility
|
||||
public bool? HasFile { get; set; }
|
||||
public int MovieFileId { get; set; }
|
||||
|
||||
// Editing Only
|
||||
public bool Monitored { get; set; }
|
||||
|
@ -80,6 +82,10 @@ namespace Radarr.Api.V3.Movies
|
|||
public MovieCollectionResource Collection { get; set; }
|
||||
public float Popularity { 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
|
||||
|
@ -118,6 +124,8 @@ namespace Radarr.Api.V3.Movies
|
|||
Year = model.Year,
|
||||
SecondaryYear = model.MovieMetadata.Value.SecondaryYear,
|
||||
|
||||
MovieFileId = model.MovieFileId,
|
||||
|
||||
Path = model.Path,
|
||||
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…
Add table
Reference in a new issue