mirror of
https://github.com/Radarr/Radarr.git
synced 2025-04-23 22:17:15 -04:00
Fixed: Changes to Profiles, Languages, Manual Import
This commit is contained in:
parent
c76364a891
commit
9350f6a04c
97 changed files with 1217 additions and 428 deletions
|
@ -1,4 +1,4 @@
|
|||
version: '0.2.0.{build}'
|
||||
version: '2.0.0.{build}'
|
||||
|
||||
image: Visual Studio 2017
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ function ErrorPage(props) {
|
|||
customFiltersError,
|
||||
tagsError,
|
||||
qualityProfilesError,
|
||||
languagesError,
|
||||
uiSettingsError,
|
||||
systemStatusError
|
||||
} = props;
|
||||
|
@ -27,6 +28,8 @@ function ErrorPage(props) {
|
|||
errorMessage = getErrorMessage(tagsError, 'Failed to load tags from API');
|
||||
} else if (qualityProfilesError) {
|
||||
errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API');
|
||||
} else if (languagesError) {
|
||||
errorMessage = getErrorMessage(languagesError, 'Failed to load languages from API');
|
||||
} else if (uiSettingsError) {
|
||||
errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API');
|
||||
} else if (systemStatusError) {
|
||||
|
@ -53,6 +56,7 @@ ErrorPage.propTypes = {
|
|||
customFiltersError: PropTypes.object,
|
||||
tagsError: PropTypes.object,
|
||||
qualityProfilesError: PropTypes.object,
|
||||
languagesError: PropTypes.object,
|
||||
uiSettingsError: PropTypes.object,
|
||||
systemStatusError: PropTypes.object
|
||||
};
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.logoLink {
|
||||
line-height: 0;
|
||||
.logoFull {
|
||||
width: 144px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
|
|
|
@ -45,19 +45,17 @@ class PageHeader extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
onSidebarToggle
|
||||
onSidebarToggle,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.logoContainer}>
|
||||
<Link
|
||||
className={styles.logoLink}
|
||||
to={`${window.Radarr.urlBase}/`}
|
||||
>
|
||||
<Link to={`${window.Radarr.urlBase}/`}>
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Radarr.urlBase}/Content/Images/logo.svg`}
|
||||
className={isSmallScreen ? styles.logo : styles.logoFull}
|
||||
src={isSmallScreen ? `${window.Radarr.urlBase}/Content/Images/logo.png` : `${window.Radarr.urlBase}/Content/Images/logo-full.png`}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -95,6 +93,7 @@ class PageHeader extends Component {
|
|||
|
||||
PageHeader.propTypes = {
|
||||
onSidebarToggle: PropTypes.func.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ class Page extends Component {
|
|||
|
||||
<PageHeader
|
||||
onSidebarToggle={onSidebarToggle}
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
|
||||
<div className={styles.main}>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
|||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchMovies } from 'Store/Actions/movieActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import { fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchQualityProfiles, fetchUISettings, fetchLanguages } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import ErrorPage from './ErrorPage';
|
||||
import LoadingPage from './LoadingPage';
|
||||
|
@ -48,6 +48,7 @@ const selectIsPopulated = createSelector(
|
|||
(state) => state.tags.isPopulated,
|
||||
(state) => state.settings.ui.isPopulated,
|
||||
(state) => state.settings.qualityProfiles.isPopulated,
|
||||
(state) => state.settings.languages.isPopulated,
|
||||
(state) => state.system.status.isPopulated,
|
||||
(
|
||||
moviesIsPopulated,
|
||||
|
@ -55,6 +56,7 @@ const selectIsPopulated = createSelector(
|
|||
tagsIsPopulated,
|
||||
uiSettingsIsPopulated,
|
||||
qualityProfilesIsPopulated,
|
||||
languagesIsPopulated,
|
||||
systemStatusIsPopulated
|
||||
) => {
|
||||
return (
|
||||
|
@ -63,6 +65,7 @@ const selectIsPopulated = createSelector(
|
|||
tagsIsPopulated &&
|
||||
uiSettingsIsPopulated &&
|
||||
qualityProfilesIsPopulated &&
|
||||
languagesIsPopulated &&
|
||||
systemStatusIsPopulated
|
||||
);
|
||||
}
|
||||
|
@ -74,6 +77,7 @@ const selectErrors = createSelector(
|
|||
(state) => state.tags.error,
|
||||
(state) => state.settings.ui.error,
|
||||
(state) => state.settings.qualityProfiles.error,
|
||||
(state) => state.settings.languages.error,
|
||||
(state) => state.system.status.error,
|
||||
(
|
||||
moviesError,
|
||||
|
@ -81,6 +85,7 @@ const selectErrors = createSelector(
|
|||
tagsError,
|
||||
uiSettingsError,
|
||||
qualityProfilesError,
|
||||
languagesError,
|
||||
systemStatusError
|
||||
) => {
|
||||
const hasError = !!(
|
||||
|
@ -89,6 +94,7 @@ const selectErrors = createSelector(
|
|||
tagsError ||
|
||||
uiSettingsError ||
|
||||
qualityProfilesError ||
|
||||
languagesError ||
|
||||
systemStatusError
|
||||
);
|
||||
|
||||
|
@ -99,6 +105,7 @@ const selectErrors = createSelector(
|
|||
tagsError,
|
||||
uiSettingsError,
|
||||
qualityProfilesError,
|
||||
languagesError,
|
||||
systemStatusError
|
||||
};
|
||||
}
|
||||
|
@ -143,6 +150,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
dispatchFetchQualityProfiles() {
|
||||
dispatch(fetchQualityProfiles());
|
||||
},
|
||||
dispatchFetchLanguages() {
|
||||
dispatch(fetchLanguages());
|
||||
},
|
||||
dispatchFetchUISettings() {
|
||||
dispatch(fetchUISettings());
|
||||
},
|
||||
|
@ -177,6 +187,7 @@ class PageConnector extends Component {
|
|||
this.props.dispatchFetchCustomFilters();
|
||||
this.props.dispatchFetchTags();
|
||||
this.props.dispatchFetchQualityProfiles();
|
||||
this.props.dispatchFetchLanguages();
|
||||
this.props.dispatchFetchUISettings();
|
||||
this.props.dispatchFetchStatus();
|
||||
}
|
||||
|
@ -199,6 +210,7 @@ class PageConnector extends Component {
|
|||
dispatchFetchMovies,
|
||||
dispatchFetchTags,
|
||||
dispatchFetchQualityProfiles,
|
||||
dispatchFetchLanguages,
|
||||
dispatchFetchUISettings,
|
||||
dispatchFetchStatus,
|
||||
...otherProps
|
||||
|
@ -236,6 +248,7 @@ PageConnector.propTypes = {
|
|||
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||
|
|
|
@ -16,24 +16,21 @@
|
|||
}
|
||||
|
||||
.leftButtons,
|
||||
.centerButtons,
|
||||
.rightButtons {
|
||||
display: flex;
|
||||
flex: 1 0 33%;
|
||||
flex: 1 0 50%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.centerButtons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.importMode {
|
||||
.importMode,
|
||||
.bulkSelect {
|
||||
composes: select from '~Components/Form/SelectInput.css';
|
||||
|
||||
margin-right: 10px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
@ -44,7 +41,6 @@
|
|||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.footer {
|
||||
.leftButtons,
|
||||
.centerButtons,
|
||||
.rightButtons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -53,10 +49,6 @@
|
|||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.centerButtons {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,9 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
|
||||
import InteractiveImportRow from './InteractiveImportRow';
|
||||
import styles from './InteractiveImportModalContent.css';
|
||||
|
||||
|
@ -32,8 +34,8 @@ const columns = [
|
|||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'series',
|
||||
label: 'Series',
|
||||
name: 'movie',
|
||||
label: 'Movie',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
|
@ -69,6 +71,16 @@ const filterExistingFilesOptions = {
|
|||
NEW: 'new'
|
||||
};
|
||||
|
||||
const importModeOptions = [
|
||||
{ key: 'move', value: 'Move Files' },
|
||||
{ key: 'copy', value: 'Copy Files' }
|
||||
];
|
||||
|
||||
const SELECT = 'select';
|
||||
const MOVIE = 'movie';
|
||||
const LANGUAGE = 'language';
|
||||
const QUALITY = 'quality';
|
||||
|
||||
class InteractiveImportModalContent extends Component {
|
||||
|
||||
//
|
||||
|
@ -83,7 +95,7 @@ class InteractiveImportModalContent extends Component {
|
|||
lastToggled: null,
|
||||
selectedState: {},
|
||||
invalidRowsSelected: [],
|
||||
isSelectSeriesModalOpen: false
|
||||
selectModalOpen: null
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -122,9 +134,17 @@ class InteractiveImportModalContent extends Component {
|
|||
}
|
||||
|
||||
onImportSelectedPress = () => {
|
||||
const selected = this.getSelectedIds();
|
||||
const {
|
||||
downloadId,
|
||||
showImportMode,
|
||||
importMode,
|
||||
onImportSelectedPress
|
||||
} = this.props;
|
||||
|
||||
this.props.onImportSelectedPress(selected, this.props.importMode);
|
||||
const selected = this.getSelectedIds();
|
||||
const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode;
|
||||
|
||||
onImportSelectedPress(selected, finalImportMode);
|
||||
}
|
||||
|
||||
onFilterExistingFilesChange = (value) => {
|
||||
|
@ -135,12 +155,12 @@ class InteractiveImportModalContent extends Component {
|
|||
this.props.onImportModeChange(value);
|
||||
}
|
||||
|
||||
onSelectSeriesPress = () => {
|
||||
this.setState({ isSelectSeriesModalOpen: true });
|
||||
onSelectModalSelect = ({ value }) => {
|
||||
this.setState({ selectModalOpen: value });
|
||||
}
|
||||
|
||||
onSelectSeriesModalClose = () => {
|
||||
this.setState({ isSelectSeriesModalOpen: false });
|
||||
onSelectModalClose = () => {
|
||||
this.setState({ selectModalOpen: null });
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -149,7 +169,7 @@ class InteractiveImportModalContent extends Component {
|
|||
render() {
|
||||
const {
|
||||
downloadId,
|
||||
allowSeriesChange,
|
||||
allowMovieChange,
|
||||
showFilterExistingFiles,
|
||||
showImportMode,
|
||||
filterExistingFiles,
|
||||
|
@ -172,17 +192,25 @@ class InteractiveImportModalContent extends Component {
|
|||
allUnselected,
|
||||
selectedState,
|
||||
invalidRowsSelected,
|
||||
isSelectSeriesModalOpen
|
||||
selectModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load manual import items');
|
||||
|
||||
const importModeOptions = [
|
||||
{ key: 'move', value: 'Move Files' },
|
||||
{ key: 'copy', value: 'Copy Files' }
|
||||
const bulkSelectOptions = [
|
||||
{ key: SELECT, value: 'Select...', disabled: true },
|
||||
{ key: LANGUAGE, value: 'Select Language' },
|
||||
{ key: QUALITY, value: 'Select Quality' }
|
||||
];
|
||||
|
||||
if (allowMovieChange) {
|
||||
bulkSelectOptions.splice(1, 0, {
|
||||
key: MOVIE,
|
||||
value: 'Select Movie'
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
|
@ -258,7 +286,7 @@ class InteractiveImportModalContent extends Component {
|
|||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
{...item}
|
||||
allowSeriesChange={allowSeriesChange}
|
||||
allowMovieChange={allowMovieChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onValidRowChange={this.onValidRowChange}
|
||||
/>
|
||||
|
@ -278,24 +306,25 @@ class InteractiveImportModalContent extends Component {
|
|||
<ModalFooter className={styles.footer}>
|
||||
<div className={styles.leftButtons}>
|
||||
{
|
||||
!downloadId && showImportMode &&
|
||||
!downloadId && showImportMode ?
|
||||
<SelectInput
|
||||
className={styles.importMode}
|
||||
name="importMode"
|
||||
value={importMode}
|
||||
values={importModeOptions}
|
||||
onChange={this.onImportModeChange}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.centerButtons}>
|
||||
{
|
||||
allowSeriesChange &&
|
||||
<Button onPress={this.onSelectSeriesPress}>
|
||||
Select Series
|
||||
</Button>
|
||||
}
|
||||
<SelectInput
|
||||
className={styles.bulkSelect}
|
||||
name="select"
|
||||
value={SELECT}
|
||||
values={bulkSelectOptions}
|
||||
isDisabled={!selectedIds.length}
|
||||
onChange={this.onSelectModalSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.rightButtons}>
|
||||
|
@ -318,10 +347,26 @@ class InteractiveImportModalContent extends Component {
|
|||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
<SelectSeriesModal
|
||||
isOpen={isSelectSeriesModalOpen}
|
||||
<SelectMovieModal
|
||||
isOpen={selectModalOpen === MOVIE}
|
||||
ids={selectedIds}
|
||||
onModalClose={this.onSelectSeriesModalClose}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectLanguageModal
|
||||
isOpen={selectModalOpen === LANGUAGE}
|
||||
ids={selectedIds}
|
||||
languageId={0}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectQualityModal
|
||||
isOpen={selectModalOpen === QUALITY}
|
||||
ids={selectedIds}
|
||||
qualityId={0}
|
||||
proper={false}
|
||||
real={false}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
|
@ -330,7 +375,7 @@ class InteractiveImportModalContent extends Component {
|
|||
|
||||
InteractiveImportModalContent.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
allowSeriesChange: PropTypes.bool.isRequired,
|
||||
allowMovieChange: PropTypes.bool.isRequired,
|
||||
showImportMode: PropTypes.bool.isRequired,
|
||||
showFilterExistingFiles: PropTypes.bool.isRequired,
|
||||
filterExistingFiles: PropTypes.bool.isRequired,
|
||||
|
@ -352,7 +397,7 @@ InteractiveImportModalContent.propTypes = {
|
|||
};
|
||||
|
||||
InteractiveImportModalContent.defaultProps = {
|
||||
allowSeriesChange: true,
|
||||
allowMovieChange: true,
|
||||
showFilterExistingFiles: false,
|
||||
showImportMode: true,
|
||||
importMode: 'move'
|
||||
|
|
|
@ -19,11 +19,11 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchInteractiveImportItems,
|
||||
setInteractiveImportSort,
|
||||
setInteractiveImportMode,
|
||||
clearInteractiveImport,
|
||||
executeCommand
|
||||
dispatchFetchInteractiveImportItems: fetchInteractiveImportItems,
|
||||
dispatchSetInteractiveImportSort: setInteractiveImportSort,
|
||||
dispatchSetInteractiveImportMode: setInteractiveImportMode,
|
||||
dispatchClearInteractiveImport: clearInteractiveImport,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class InteractiveImportModalContentConnector extends Component {
|
||||
|
@ -50,7 +50,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
filterExistingFiles
|
||||
} = this.state;
|
||||
|
||||
this.props.fetchInteractiveImportItems({
|
||||
this.props.dispatchFetchInteractiveImportItems({
|
||||
downloadId,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
|
@ -68,7 +68,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
folder
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchInteractiveImportItems({
|
||||
this.props.dispatchFetchInteractiveImportItems({
|
||||
downloadId,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
|
@ -77,14 +77,14 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearInteractiveImport();
|
||||
this.props.dispatchClearInteractiveImport();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey, sortDirection) => {
|
||||
this.props.setInteractiveImportSort({ sortKey, sortDirection });
|
||||
this.props.dispatchSetInteractiveImportSort({ sortKey, sortDirection });
|
||||
}
|
||||
|
||||
onFilterExistingFilesChange = (filterExistingFiles) => {
|
||||
|
@ -92,7 +92,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
}
|
||||
|
||||
onImportModeChange = (importMode) => {
|
||||
this.props.setInteractiveImportMode({ importMode });
|
||||
this.props.dispatchSetInteractiveImportMode({ importMode });
|
||||
}
|
||||
|
||||
onImportSelectedPress = (selected, importMode) => {
|
||||
|
@ -103,25 +103,13 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
|
||||
if (isSelected) {
|
||||
const {
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
movie,
|
||||
quality,
|
||||
language
|
||||
} = item;
|
||||
|
||||
if (!series) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNaN(seasonNumber)) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!episodes || !episodes.length) {
|
||||
this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' });
|
||||
if (!movie) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Movie must be chosen for each selected file' });
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -138,8 +126,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
files.push({
|
||||
path: item.path,
|
||||
folderName: item.folderName,
|
||||
seriesId: series.id,
|
||||
episodeIds: _.map(episodes, 'id'),
|
||||
movieId: movie.id,
|
||||
quality,
|
||||
language,
|
||||
downloadId: this.props.downloadId
|
||||
|
@ -151,7 +138,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.executeCommand({
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.INTERACTIVE_IMPORT,
|
||||
files,
|
||||
importMode
|
||||
|
@ -188,11 +175,11 @@ InteractiveImportModalContentConnector.propTypes = {
|
|||
folder: PropTypes.string,
|
||||
filterExistingFiles: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchInteractiveImportItems: PropTypes.func.isRequired,
|
||||
setInteractiveImportSort: PropTypes.func.isRequired,
|
||||
clearInteractiveImport: PropTypes.func.isRequired,
|
||||
setInteractiveImportMode: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
dispatchFetchInteractiveImportItems: PropTypes.func.isRequired,
|
||||
dispatchSetInteractiveImportSort: PropTypes.func.isRequired,
|
||||
dispatchSetInteractiveImportMode: PropTypes.func.isRequired,
|
||||
dispatchClearInteractiveImport: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -9,9 +9,10 @@ import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
|
|||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
// import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
// import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||
import styles from './InteractiveImportRow.css';
|
||||
|
||||
|
@ -24,21 +25,24 @@ class InteractiveImportRow extends Component {
|
|||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isSelectSeriesModalOpen: false,
|
||||
isSelectQualityModalOpen: false
|
||||
isSelectMovieModalOpen: false,
|
||||
isSelectQualityModalOpen: false,
|
||||
isSelectLanguageModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
id,
|
||||
series,
|
||||
quality
|
||||
movie,
|
||||
quality,
|
||||
language
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
series &&
|
||||
quality
|
||||
movie &&
|
||||
quality &&
|
||||
language
|
||||
) {
|
||||
this.props.onSelectedChange({ id, value: true });
|
||||
}
|
||||
|
@ -47,23 +51,26 @@ class InteractiveImportRow extends Component {
|
|||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
series,
|
||||
movie,
|
||||
quality,
|
||||
language,
|
||||
isSelected,
|
||||
onValidRowChange
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
prevProps.series === series &&
|
||||
prevProps.movie === movie &&
|
||||
prevProps.quality === quality &&
|
||||
prevProps.language === language &&
|
||||
prevProps.isSelected === isSelected
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = !!(
|
||||
series &&
|
||||
quality
|
||||
movie &&
|
||||
quality &&
|
||||
language
|
||||
);
|
||||
|
||||
if (isSelected && !isValid) {
|
||||
|
@ -90,16 +97,20 @@ class InteractiveImportRow extends Component {
|
|||
//
|
||||
// Listeners
|
||||
|
||||
onSelectSeriesPress = () => {
|
||||
this.setState({ isSelectSeriesModalOpen: true });
|
||||
onSelectMoviePress = () => {
|
||||
this.setState({ isSelectMovieModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectQualityPress = () => {
|
||||
this.setState({ isSelectQualityModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectSeriesModalClose = (changed) => {
|
||||
this.setState({ isSelectSeriesModalOpen: false });
|
||||
onSelectLanguagePress = () => {
|
||||
this.setState({ isSelectLanguageModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectMovieModalClose = (changed) => {
|
||||
this.setState({ isSelectMovieModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
}
|
||||
|
||||
|
@ -108,16 +119,22 @@ class InteractiveImportRow extends Component {
|
|||
this.selectRowAfterChange(changed);
|
||||
}
|
||||
|
||||
onSelectLanguageModalClose = (changed) => {
|
||||
this.setState({ isSelectLanguageModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
allowSeriesChange,
|
||||
allowMovieChange,
|
||||
relativePath,
|
||||
series,
|
||||
movie,
|
||||
quality,
|
||||
language,
|
||||
size,
|
||||
rejections,
|
||||
isSelected,
|
||||
|
@ -125,14 +142,16 @@ class InteractiveImportRow extends Component {
|
|||
} = this.props;
|
||||
|
||||
const {
|
||||
isSelectSeriesModalOpen,
|
||||
isSelectQualityModalOpen
|
||||
isSelectMovieModalOpen,
|
||||
isSelectQualityModalOpen,
|
||||
isSelectLanguageModalOpen
|
||||
} = this.state;
|
||||
|
||||
const seriesTitle = series ? series.title : '';
|
||||
const movieTitle = movie ? movie.title : '';
|
||||
|
||||
const showSeriesPlaceholder = isSelected && !series;
|
||||
const showMoviePlaceholder = isSelected && !movie;
|
||||
const showQualityPlaceholder = isSelected && !quality;
|
||||
const showLanguagePlaceholder = isSelected && !language;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
|
@ -150,16 +169,18 @@ class InteractiveImportRow extends Component {
|
|||
</TableRowCell>
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!allowSeriesChange}
|
||||
onPress={this.onSelectSeriesPress}
|
||||
isDisabled={!allowMovieChange}
|
||||
title={allowMovieChange ? 'Click to change movie' : undefined}
|
||||
onPress={this.onSelectMoviePress}
|
||||
>
|
||||
{
|
||||
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
|
||||
showMoviePlaceholder ? <InteractiveImportRowCellPlaceholder /> : movieTitle
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
className={styles.quality}
|
||||
title="Click to change quality"
|
||||
onPress={this.onSelectQualityPress}
|
||||
>
|
||||
{
|
||||
|
@ -176,6 +197,25 @@ class InteractiveImportRow extends Component {
|
|||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
className={styles.language}
|
||||
title="Click to change language"
|
||||
onPress={this.onSelectLanguagePress}
|
||||
>
|
||||
{
|
||||
showLanguagePlaceholder &&
|
||||
<InteractiveImportRowCellPlaceholder />
|
||||
}
|
||||
|
||||
{/* {
|
||||
!showLanguagePlaceholder && !!language &&
|
||||
<MovieLanguage
|
||||
className={styles.label}
|
||||
language={language}
|
||||
/>
|
||||
} */}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCell>
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
|
@ -209,20 +249,27 @@ class InteractiveImportRow extends Component {
|
|||
}
|
||||
</TableRowCell>
|
||||
|
||||
<SelectSeriesModal
|
||||
isOpen={isSelectSeriesModalOpen}
|
||||
<SelectMovieModal
|
||||
isOpen={isSelectMovieModalOpen}
|
||||
ids={[id]}
|
||||
onModalClose={this.onSelectSeriesModalClose}
|
||||
onModalClose={this.onSelectMovieModalClose}
|
||||
/>
|
||||
|
||||
<SelectQualityModal
|
||||
isOpen={isSelectQualityModalOpen}
|
||||
id={id}
|
||||
ids={[id]}
|
||||
qualityId={quality ? quality.quality.id : 0}
|
||||
proper={quality ? quality.revision.version > 1 : false}
|
||||
real={quality ? quality.revision.real > 0 : false}
|
||||
onModalClose={this.onSelectQualityModalClose}
|
||||
/>
|
||||
|
||||
<SelectLanguageModal
|
||||
isOpen={isSelectLanguageModalOpen}
|
||||
ids={[id]}
|
||||
languageId={language ? language.id : 0}
|
||||
onModalClose={this.onSelectLanguageModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
@ -231,12 +278,11 @@ class InteractiveImportRow extends Component {
|
|||
|
||||
InteractiveImportRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
allowSeriesChange: PropTypes.bool.isRequired,
|
||||
allowMovieChange: PropTypes.bool.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
series: PropTypes.object,
|
||||
seasonNumber: PropTypes.number,
|
||||
episodes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
movie: PropTypes.object,
|
||||
quality: PropTypes.object,
|
||||
language: PropTypes.object,
|
||||
size: PropTypes.number.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
|
@ -244,8 +290,4 @@ InteractiveImportRow.propTypes = {
|
|||
onValidRowChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportRow.defaultProps = {
|
||||
episodes: []
|
||||
};
|
||||
|
||||
export default InteractiveImportRow;
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectLanguageModalContentConnector from './SelectLanguageModalContentConnector';
|
||||
|
||||
class SelectLanguageModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectLanguageModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectLanguageModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectLanguageModal;
|
|
@ -0,0 +1,87 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
||||
function SelectLanguageModalContent(props) {
|
||||
const {
|
||||
languageId,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
onModalClose,
|
||||
onLanguageSelect
|
||||
} = props;
|
||||
|
||||
const languageOptions = items.map(( language ) => {
|
||||
return {
|
||||
key: language.id,
|
||||
value: language.name
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Language
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load languages</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error &&
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Language</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="language"
|
||||
value={languageId}
|
||||
values={languageOptions}
|
||||
onChange={onLanguageSelect}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
SelectLanguageModalContent.propTypes = {
|
||||
languageId: PropTypes.number.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onLanguageSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectLanguageModalContent;
|
|
@ -0,0 +1,88 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchLanguages } from 'Store/Actions/settingsActions';
|
||||
import { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||
import SelectLanguageModalContent from './SelectLanguageModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.languages,
|
||||
(languages) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = languages;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchLanguages: fetchLanguages,
|
||||
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems
|
||||
};
|
||||
|
||||
class SelectLanguageModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount = () => {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchLanguages();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onLanguageSelect = ({ value }) => {
|
||||
const languageId = parseInt(value);
|
||||
|
||||
const language = _.find(this.props.items,
|
||||
(item) => item.id === languageId);
|
||||
|
||||
this.props.dispatchUpdateInteractiveImportItems({
|
||||
ids: this.props.ids,
|
||||
language
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectLanguageModalContent
|
||||
{...this.props}
|
||||
onLanguageSelect={this.onLanguageSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectLanguageModalContentConnector.propTypes = {
|
||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SelectLanguageModalContentConnector);
|
|
@ -1,9 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectSeriesModalContentConnector from './SelectSeriesModalContentConnector';
|
||||
import SelectMovieModalContentConnector from './SelectMovieModalContentConnector';
|
||||
|
||||
class SelectSeriesModal extends Component {
|
||||
class SelectMovieModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
@ -20,7 +20,7 @@ class SelectSeriesModal extends Component {
|
|||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectSeriesModalContentConnector
|
||||
<SelectMovieModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
|
@ -29,9 +29,9 @@ class SelectSeriesModal extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
SelectSeriesModal.propTypes = {
|
||||
SelectMovieModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectSeriesModal;
|
||||
export default SelectMovieModal;
|
|
@ -8,10 +8,10 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import SelectSeriesRow from './SelectSeriesRow';
|
||||
import styles from './SelectSeriesModalContent.css';
|
||||
import SelectMovieRow from './SelectMovieRow';
|
||||
import styles from './SelectMovieModalContent.css';
|
||||
|
||||
class SelectSeriesModalContent extends Component {
|
||||
class SelectMovieModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
@ -46,7 +46,7 @@ class SelectSeriesModalContent extends Component {
|
|||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Series
|
||||
Manual Import - Select Movie
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
|
@ -55,7 +55,7 @@ class SelectSeriesModalContent extends Component {
|
|||
>
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder="Filter series"
|
||||
placeholder="Filter movie"
|
||||
name="filter"
|
||||
value={filter}
|
||||
autoFocus={true}
|
||||
|
@ -67,7 +67,7 @@ class SelectSeriesModalContent extends Component {
|
|||
items.map((item) => {
|
||||
return item.title.toLowerCase().includes(filter) ?
|
||||
(
|
||||
<SelectSeriesRow
|
||||
<SelectMovieRow
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
title={item.title}
|
||||
|
@ -90,10 +90,10 @@ class SelectSeriesModalContent extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
SelectSeriesModalContent.propTypes = {
|
||||
SelectMovieModalContent.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMovieSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectSeriesModalContent;
|
||||
export default SelectMovieModalContent;
|
|
@ -1,11 +1,10 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import SelectSeriesModalContent from './SelectSeriesModalContent';
|
||||
import SelectMovieModalContent from './SelectMovieModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
|
@ -29,27 +28,32 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateInteractiveImportItem
|
||||
dispatchUpdateInteractiveImportItem: updateInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectSeriesModalContentConnector extends Component {
|
||||
class SelectMovieModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMovieSelect = (seriesId) => {
|
||||
const series = _.find(this.props.items, { id: seriesId });
|
||||
onMovieSelect = (movieId) => {
|
||||
const {
|
||||
ids,
|
||||
items,
|
||||
dispatchUpdateInteractiveImportItem,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
this.props.ids.forEach((id) => {
|
||||
this.props.updateInteractiveImportItem({
|
||||
const movie = items.find((s) => s.id === movieId);
|
||||
|
||||
ids.forEach((id) => {
|
||||
dispatchUpdateInteractiveImportItem({
|
||||
id,
|
||||
series,
|
||||
seasonNumber: undefined,
|
||||
episodes: []
|
||||
movie
|
||||
});
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -57,7 +61,7 @@ class SelectSeriesModalContentConnector extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<SelectSeriesModalContent
|
||||
<SelectMovieModalContent
|
||||
{...this.props}
|
||||
onMovieSelect={this.onMovieSelect}
|
||||
/>
|
||||
|
@ -65,11 +69,11 @@ class SelectSeriesModalContentConnector extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
SelectSeriesModalContentConnector.propTypes = {
|
||||
SelectMovieModalContentConnector.propTypes = {
|
||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
dispatchUpdateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SelectSeriesModalContentConnector);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SelectMovieModalContentConnector);
|
|
@ -1,4 +1,4 @@
|
|||
.series {
|
||||
.movie {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './SelectSeriesRow.css';
|
||||
import styles from './SelectMovieRow.css';
|
||||
|
||||
class SelectSeriesRow extends Component {
|
||||
class SelectMovieRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
@ -18,7 +18,7 @@ class SelectSeriesRow extends Component {
|
|||
render() {
|
||||
return (
|
||||
<Link
|
||||
className={styles.series}
|
||||
className={styles.movie}
|
||||
component="div"
|
||||
onPress={this.onPress}
|
||||
>
|
||||
|
@ -28,10 +28,10 @@ class SelectSeriesRow extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
SelectSeriesRow.propTypes = {
|
||||
SelectMovieRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onMovieSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectSeriesRow;
|
||||
export default SelectMovieRow;
|
|
@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import getQualities from 'Utilities/Quality/getQualities';
|
||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||
import SelectQualityModalContent from './SelectQualityModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -30,8 +30,8 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchQualityProfileSchema,
|
||||
updateInteractiveImportItem
|
||||
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema,
|
||||
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems
|
||||
};
|
||||
|
||||
class SelectQualityModalContentConnector extends Component {
|
||||
|
@ -41,7 +41,7 @@ class SelectQualityModalContentConnector extends Component {
|
|||
|
||||
componentDidMount = () => {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.fetchQualityProfileSchema();
|
||||
this.props.dispatchFetchQualityProfileSchema();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,8 +57,8 @@ class SelectQualityModalContentConnector extends Component {
|
|||
real: real ? 1 : 0
|
||||
};
|
||||
|
||||
this.props.updateInteractiveImportItem({
|
||||
id: this.props.id,
|
||||
this.props.dispatchUpdateInteractiveImportItems({
|
||||
ids: this.props.ids,
|
||||
quality: {
|
||||
quality,
|
||||
revision
|
||||
|
@ -82,13 +82,13 @@ class SelectQualityModalContentConnector extends Component {
|
|||
}
|
||||
|
||||
SelectQualityModalContentConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import MovieIndexFilterMenu from './Menus/MovieIndexFilterMenu';
|
|||
import MovieIndexSortMenu from './Menus/MovieIndexSortMenu';
|
||||
import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
|
||||
import MovieIndexFooterConnector from './MovieIndexFooterConnector';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import styles from './MovieIndex.css';
|
||||
|
||||
function getViewComponent(view) {
|
||||
|
@ -49,6 +50,7 @@ class MovieIndex extends Component {
|
|||
jumpToCharacter: null,
|
||||
isPosterOptionsModalOpen: false,
|
||||
isOverviewOptionsModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isRendered: false
|
||||
};
|
||||
}
|
||||
|
@ -137,6 +139,14 @@ class MovieIndex extends Component {
|
|||
this.setState({ isOverviewOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
}
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
|
||||
onJumpBarItemPress = (jumpToCharacter) => {
|
||||
this.setState({ jumpToCharacter });
|
||||
}
|
||||
|
@ -195,6 +205,7 @@ class MovieIndex extends Component {
|
|||
jumpToCharacter,
|
||||
isPosterOptionsModalOpen,
|
||||
isOverviewOptionsModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isRendered
|
||||
} = this.state;
|
||||
|
||||
|
@ -223,6 +234,28 @@ class MovieIndex extends Component {
|
|||
onPress={onRssSyncPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Search Missing"
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={hasNoMovie}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Manual Import"
|
||||
iconName={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Edit Mode"
|
||||
iconName={icons.EDIT}
|
||||
isDisabled={hasNoMovie}
|
||||
/>
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
|
@ -339,6 +372,11 @@ class MovieIndex extends Component {
|
|||
isOpen={isOverviewOptionsModalOpen}
|
||||
onModalClose={this.onOverviewOptionsModalClose}
|
||||
/>
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
.added,
|
||||
.inCinemas,
|
||||
.physicalRelease,
|
||||
.genres {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
|
|
|
@ -92,10 +92,12 @@ class EditQualityProfileModalContent extends Component {
|
|||
isSaving,
|
||||
saveError,
|
||||
qualities,
|
||||
languages,
|
||||
item,
|
||||
isInUse,
|
||||
onInputChange,
|
||||
onCutoffChange,
|
||||
onLanguageChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
onDeleteQualityProfilePress,
|
||||
|
@ -105,10 +107,14 @@ class EditQualityProfileModalContent extends Component {
|
|||
const {
|
||||
id,
|
||||
name,
|
||||
upgradeAllowed,
|
||||
cutoff,
|
||||
language,
|
||||
items
|
||||
} = item;
|
||||
|
||||
const languageId = language.value.id;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<Measure
|
||||
|
@ -159,16 +165,48 @@ class EditQualityProfileModalContent extends Component {
|
|||
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Cutoff
|
||||
Upgrades Allowed
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="upgradeAllowed"
|
||||
{...upgradeAllowed}
|
||||
helpText="If disabled qualities will not be upgraded"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
upgradeAllowed.value &&
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Upgrade Until
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="cutoff"
|
||||
{...cutoff}
|
||||
values={qualities}
|
||||
helpText="Once this quality is reached Radarr will no longer download movies"
|
||||
onChange={onCutoffChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Language
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="cutoff"
|
||||
{...cutoff}
|
||||
values={qualities}
|
||||
helpText="Once this quality is reached Radarr will no longer download movies"
|
||||
onChange={onCutoffChange}
|
||||
name="language"
|
||||
values={languages}
|
||||
value={languageId}
|
||||
helpText="Language for Releases"
|
||||
onChange={onLanguageChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
@ -197,10 +235,10 @@ class EditQualityProfileModalContent extends Component {
|
|||
>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
id ?
|
||||
<div
|
||||
className={styles.deleteButtonContainer}
|
||||
title={isInUse && 'Can\'t delete a quality profile that is attached to a series'}
|
||||
title={isInUse ? 'Can\'t delete a quality profile that is attached to a movie' : undefined}
|
||||
>
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
|
@ -209,7 +247,8 @@ class EditQualityProfileModalContent extends Component {
|
|||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<Button
|
||||
|
@ -239,10 +278,12 @@ EditQualityProfileModalContent.propTypes = {
|
|||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
isInUse: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onCutoffChange: PropTypes.func.isRequired,
|
||||
onLanguageChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onContentHeightChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
|
|
|
@ -61,14 +61,38 @@ function createQualitiesSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
function createLanguagesSelector() {
|
||||
return createSelector(
|
||||
(state) => state.settings.languages,
|
||||
(languages) => {
|
||||
const items = languages.items;
|
||||
|
||||
if (!items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const newItems = items.map((item) => {
|
||||
return {
|
||||
key: item.id,
|
||||
value: item.name
|
||||
};
|
||||
});
|
||||
|
||||
return newItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createProviderSettingsSelector('qualityProfiles'),
|
||||
createQualitiesSelector(),
|
||||
createLanguagesSelector(),
|
||||
createProfileInUseSelector('qualityProfileId'),
|
||||
(qualityProfile, qualities, isInUse) => {
|
||||
(qualityProfile, qualities, languages, isInUse) => {
|
||||
return {
|
||||
qualities,
|
||||
languages,
|
||||
...qualityProfile,
|
||||
isInUse
|
||||
};
|
||||
|
@ -159,6 +183,15 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||
this.props.setQualityProfileValue({ name, value: cutoffId });
|
||||
}
|
||||
|
||||
onLanguageChange = ({ name, value }) => {
|
||||
|
||||
const id = parseInt(value);
|
||||
|
||||
const language = _.find(this.props.languages, (item) => item.key === id);
|
||||
|
||||
this.props.setQualityProfileValue({ name, value: { id: language.key, Name: language.value } });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveQualityProfile({ id: this.props.id });
|
||||
}
|
||||
|
@ -413,6 +446,7 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
onCutoffChange={this.onCutoffChange}
|
||||
onLanguageChange={this.onLanguageChange}
|
||||
onCreateGroupPress={this.onCreateGroupPress}
|
||||
onDeleteGroupPress={this.onDeleteGroupPress}
|
||||
onQualityProfileItemAllowedChange={this.onQualityProfileItemAllowedChange}
|
||||
|
@ -433,6 +467,7 @@ EditQualityProfileModalContentConnector.propTypes = {
|
|||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
setQualityProfileValue: PropTypes.func.isRequired,
|
||||
fetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
saveQualityProfile: PropTypes.func.isRequired,
|
||||
|
|
|
@ -65,6 +65,7 @@ class QualityProfile extends Component {
|
|||
const {
|
||||
id,
|
||||
name,
|
||||
upgradeAllowed,
|
||||
cutoff,
|
||||
items,
|
||||
isDeleting
|
||||
|
@ -97,7 +98,7 @@ class QualityProfile extends Component {
|
|||
}
|
||||
|
||||
if (item.quality) {
|
||||
const isCutoff = item.quality.id === cutoff;
|
||||
const isCutoff = upgradeAllowed && item.quality.id === cutoff;
|
||||
|
||||
return (
|
||||
<Label
|
||||
|
@ -110,7 +111,7 @@ class QualityProfile extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
const isCutoff = item.id === cutoff;
|
||||
const isCutoff = upgradeAllowed && item.id === cutoff;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
@ -174,6 +175,7 @@ class QualityProfile extends Component {
|
|||
QualityProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
upgradeAllowed: PropTypes.bool.isRequired,
|
||||
cutoff: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
|
|
48
frontend/src/Store/Actions/Settings/languages.js
Normal file
48
frontend/src/Store/Actions/Settings/languages.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { createThunk } from 'Store/thunks';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.languages';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_LANGUAGES = 'settings/languages/fetchLanguages';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchLanguages = createThunk(FETCH_LANGUAGES);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_LANGUAGES]: createFetchHandler(section, '/language')
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
|
||||
}
|
||||
|
||||
};
|
|
@ -2,11 +2,9 @@ import moment from 'moment';
|
|||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { set, update } from './baseActions';
|
||||
|
||||
|
@ -15,8 +13,6 @@ import { set, update } from './baseActions';
|
|||
|
||||
export const section = 'interactiveImport';
|
||||
|
||||
const episodesSection = `${section}.episodes`;
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
|
@ -36,24 +32,15 @@ export const defaultState = {
|
|||
return relativePath.toLowerCase();
|
||||
},
|
||||
|
||||
series: function(item, direction) {
|
||||
const series = item.series;
|
||||
movie: function(item, direction) {
|
||||
const movie = item.movie;
|
||||
|
||||
return series ? series.sortTitle : '';
|
||||
return movie ? movie.sortTitle : '';
|
||||
},
|
||||
|
||||
quality: function(item, direction) {
|
||||
return item.quality ? item.quality.qualityWeight : 0;
|
||||
}
|
||||
},
|
||||
|
||||
episodes: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
sortKey: 'episodeNumber',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
items: []
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -68,30 +55,24 @@ export const persistState = [
|
|||
export const FETCH_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/fetchInteractiveImportItems';
|
||||
export const SET_INTERACTIVE_IMPORT_SORT = 'interactiveImport/setInteractiveImportSort';
|
||||
export const UPDATE_INTERACTIVE_IMPORT_ITEM = 'interactiveImport/updateInteractiveImportItem';
|
||||
export const UPDATE_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/updateInteractiveImportItems';
|
||||
export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport';
|
||||
export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
|
||||
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
|
||||
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
|
||||
|
||||
export const FETCH_INTERACTIVE_IMPORT_EPISODES = 'interactiveImport/fetchInteractiveImportEpisodes';
|
||||
export const SET_INTERACTIVE_IMPORT_EPISODES_SORT = 'interactiveImport/setInteractiveImportEpisodesSort';
|
||||
export const CLEAR_INTERACTIVE_IMPORT_EPISODES = 'interactiveImport/clearInteractiveImportEpisodes';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchInteractiveImportItems = createThunk(FETCH_INTERACTIVE_IMPORT_ITEMS);
|
||||
export const setInteractiveImportSort = createAction(SET_INTERACTIVE_IMPORT_SORT);
|
||||
export const updateInteractiveImportItem = createAction(UPDATE_INTERACTIVE_IMPORT_ITEM);
|
||||
export const updateInteractiveImportItems = createAction(UPDATE_INTERACTIVE_IMPORT_ITEMS);
|
||||
export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT);
|
||||
export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
|
||||
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
|
||||
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
|
||||
|
||||
export const fetchInteractiveImportEpisodes = createThunk(FETCH_INTERACTIVE_IMPORT_EPISODES);
|
||||
export const setInteractiveImportEpisodesSort = createAction(SET_INTERACTIVE_IMPORT_EPISODES_SORT);
|
||||
export const clearInteractiveImportEpisodes = createAction(CLEAR_INTERACTIVE_IMPORT_EPISODES);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
export const actionHandlers = handleThunks({
|
||||
|
@ -129,9 +110,7 @@ export const actionHandlers = handleThunks({
|
|||
error: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[FETCH_INTERACTIVE_IMPORT_EPISODES]: createFetchHandler('interactiveImport.episodes', '/episode')
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
@ -152,6 +131,23 @@ export const reducers = createHandleActions({
|
|||
return newState;
|
||||
},
|
||||
|
||||
[UPDATE_INTERACTIVE_IMPORT_ITEMS]: (state, { payload }) => {
|
||||
const ids = payload.ids;
|
||||
const newState = Object.assign({}, state);
|
||||
const items = [...newState.items];
|
||||
|
||||
ids.forEach((id) => {
|
||||
const index = items.findIndex((item) => item.id === id);
|
||||
const item = Object.assign({}, items[index], payload);
|
||||
|
||||
items.splice(index, 1, item);
|
||||
});
|
||||
|
||||
newState.items = items;
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
[ADD_RECENT_FOLDER]: function(state, { payload }) {
|
||||
const folder = payload.folder;
|
||||
const recentFolder = { folder, lastUsed: moment().toISOString() };
|
||||
|
@ -191,14 +187,6 @@ export const reducers = createHandleActions({
|
|||
|
||||
[SET_INTERACTIVE_IMPORT_MODE]: function(state, { payload }) {
|
||||
return Object.assign({}, state, { importMode: payload.importMode });
|
||||
},
|
||||
|
||||
[SET_INTERACTIVE_IMPORT_EPISODES_SORT]: createSetClientSideCollectionSortReducer(episodesSection),
|
||||
|
||||
[CLEAR_INTERACTIVE_IMPORT_EPISODES]: (state) => {
|
||||
return updateSectionState(state, episodesSection, {
|
||||
...defaultState.episodes
|
||||
});
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
|
@ -8,6 +8,7 @@ import downloadClientOptions from './Settings/downloadClientOptions';
|
|||
import general from './Settings/general';
|
||||
import indexerOptions from './Settings/indexerOptions';
|
||||
import indexers from './Settings/indexers';
|
||||
import languages from './Settings/languages';
|
||||
import netImportOptions from './Settings/netImportOptions';
|
||||
import netImports from './Settings/netImports';
|
||||
import mediaManagement from './Settings/mediaManagement';
|
||||
|
@ -28,6 +29,7 @@ export * from './Settings/downloadClientOptions';
|
|||
export * from './Settings/general';
|
||||
export * from './Settings/indexerOptions';
|
||||
export * from './Settings/indexers';
|
||||
export * from './Settings/languages';
|
||||
export * from './Settings/netImportOptions';
|
||||
export * from './Settings/netImports';
|
||||
export * from './Settings/mediaManagement';
|
||||
|
@ -59,6 +61,7 @@ export const defaultState = {
|
|||
general: general.defaultState,
|
||||
indexerOptions: indexerOptions.defaultState,
|
||||
indexers: indexers.defaultState,
|
||||
languages: languages.defaultState,
|
||||
netImportOptions: netImportOptions.defaultState,
|
||||
netImports: netImports.defaultState,
|
||||
mediaManagement: mediaManagement.defaultState,
|
||||
|
@ -98,6 +101,7 @@ export const actionHandlers = handleThunks({
|
|||
...general.actionHandlers,
|
||||
...indexerOptions.actionHandlers,
|
||||
...indexers.actionHandlers,
|
||||
...languages.actionHandlers,
|
||||
...netImportOptions.actionHandlers,
|
||||
...netImports.actionHandlers,
|
||||
...mediaManagement.actionHandlers,
|
||||
|
@ -128,6 +132,7 @@ export const reducers = createHandleActions({
|
|||
...general.reducers,
|
||||
...indexerOptions.reducers,
|
||||
...indexers.reducers,
|
||||
...languages.reducers,
|
||||
...netImportOptions.reducers,
|
||||
...netImports.reducers,
|
||||
...mediaManagement.reducers,
|
||||
|
|
|
@ -23,11 +23,11 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>2.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>xmmd</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<string>2.0</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<string>YES</string>
|
||||
</dict>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
#define AppURL "https://radarr.video/"
|
||||
#define ForumsURL "https://github.com/Radarr/Radarr/issues"
|
||||
#define AppExeName "Radarr.exe"
|
||||
#define BuildNumber "1.0"
|
||||
#define BuildNumber "2.0"
|
||||
#define BuildVersion GetEnv('APPVEYOR_BUILD_VERSION')
|
||||
#define BranchName StringChange(GetEnv('APPVEYOR_REPO_BRANCH'), "/", "-")
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
|||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{56C1065D-3523-4025-B76D-6F73F67F7F82}
|
||||
AppName={#AppName}
|
||||
AppVersion=0.2
|
||||
AppVersion=2.0
|
||||
AppPublisher={#AppPublisher}
|
||||
AppPublisherURL={#AppURL}
|
||||
AppSupportURL={#ForumsURL}
|
||||
|
|
|
@ -4,22 +4,22 @@ using Nancy;
|
|||
using Radarr.Http.Extensions;
|
||||
using NzbDrone.Api.Movies;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.History;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.REST;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
|
||||
namespace NzbDrone.Api.History
|
||||
{
|
||||
public class HistoryModule : RadarrRestModule<HistoryResource>
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly IFailedDownloadService _failedDownloadService;
|
||||
|
||||
public HistoryModule(IHistoryService historyService,
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
IFailedDownloadService failedDownloadService)
|
||||
{
|
||||
_historyService = historyService;
|
||||
|
|
|
@ -8,8 +8,8 @@ using NzbDrone.Core.MediaFiles;
|
|||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.SignalR;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
|
||||
namespace NzbDrone.Api.MovieFiles
|
||||
{
|
||||
|
@ -18,14 +18,14 @@ namespace NzbDrone.Api.MovieFiles
|
|||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly IRecycleBinProvider _recycleBinProvider;
|
||||
private readonly IMovieService _movieService;
|
||||
private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public MovieFileModule(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IMediaFileService mediaFileService,
|
||||
IRecycleBinProvider recycleBinProvider,
|
||||
IMovieService movieService,
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
Logger logger)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using NzbDrone.Api.Movies;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
@ -15,10 +15,10 @@ namespace NzbDrone.Api.Movies
|
|||
IHandle<MovieDownloadedEvent>
|
||||
{
|
||||
protected readonly IMovieService _movieService;
|
||||
protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
protected readonly IUpgradableSpecification _qualityUpgradableSpecification;
|
||||
|
||||
protected MovieModuleWithSignalR(IMovieService movieService,
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
IBroadcastSignalRMessage signalRBroadcaster)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
|
@ -29,7 +29,7 @@ namespace NzbDrone.Api.Movies
|
|||
}
|
||||
|
||||
protected MovieModuleWithSignalR(IMovieService movieService,
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
string resource)
|
||||
: base(signalRBroadcaster, resource)
|
||||
|
|
|
@ -21,7 +21,6 @@ namespace NzbDrone.Api.Profiles
|
|||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Cutoff).NotNull();
|
||||
SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality();
|
||||
SharedValidator.RuleFor(c => c.Language).ValidLanguage();
|
||||
SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
|
||||
{
|
||||
var all = _formatService.All().Select(f => f.Id).ToList();
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
using System.Linq;
|
||||
using NzbDrone.Api.Movies;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.SignalR;
|
||||
using Radarr.Http;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
|
||||
namespace NzbDrone.Api.Wanted
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ namespace NzbDrone.Api.Wanted
|
|||
|
||||
public MovieCutoffModule(IMovieCutoffService movieCutoffService,
|
||||
IMovieService movieService,
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
IBroadcastSignalRMessage signalRBroadcaster)
|
||||
: base(movieService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/cutoff")
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using System.Linq;
|
||||
using NzbDrone.Api.Movies;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.SignalR;
|
||||
|
@ -11,7 +11,7 @@ namespace NzbDrone.Api.Wanted
|
|||
class MovieMissingModule : MovieModuleWithSignalR
|
||||
{
|
||||
public MovieMissingModule(IMovieService movieService,
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
IBroadcastSignalRMessage signalRBroadcaster)
|
||||
: base(movieService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/missing")
|
||||
{
|
||||
|
|
|
@ -3,7 +3,7 @@ using FluentAssertions;
|
|||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Test.CustomFormat;
|
||||
|
@ -11,7 +11,7 @@ using NzbDrone.Core.Test.CustomFormat;
|
|||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CutoffSpecificationFixture : CoreTest<QualityUpgradableSpecification>
|
||||
public class CutoffSpecificationFixture : CoreTest<UpgradableSpecification>
|
||||
{
|
||||
|
||||
private CustomFormats.CustomFormat _customFormat;
|
||||
|
|
|
@ -13,6 +13,7 @@ using NzbDrone.Test.Common;
|
|||
using FizzWare.NBuilder;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
|
|
|
@ -12,9 +12,8 @@ using NzbDrone.Core.Parser.Model;
|
|||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
|
@ -33,7 +32,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Mocker.Resolve<QualityUpgradableSpecification>();
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
_upgradeHistory = Mocker.Resolve<HistorySpecification>();
|
||||
|
||||
_fakeMovie = Builder<Movie>.CreateNew()
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class QualityUpgradeSpecificationFixture : CoreTest<QualityUpgradableSpecification>
|
||||
public class QualityUpgradeSpecificationFixture : CoreTest<UpgradableSpecification>
|
||||
{
|
||||
public static object[] IsUpgradeTestCases =
|
||||
{
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.Linq;
|
|||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles;
|
||||
|
@ -25,7 +24,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Mocker.Resolve<QualityUpgradableSpecification>();
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
|
||||
_movie = Builder<Movie>.CreateNew()
|
||||
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||
|
|
|
@ -6,7 +6,7 @@ using FluentAssertions;
|
|||
using Marr.Data;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
|
||||
private void GivenUpgradeForExistingFile()
|
||||
{
|
||||
Mocker.GetMock<IQualityUpgradableSpecification>()
|
||||
Mocker.GetMock<IUpgradableSpecification>()
|
||||
.Setup(s => s.IsUpgradable(It.IsAny<Profile>(), It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
|
||||
.Returns(true);
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
GivenExistingFile(new QualityModel(Quality.HDTV720p));
|
||||
GivenUpgradeForExistingFile();
|
||||
|
||||
Mocker.GetMock<IQualityUpgradableSpecification>()
|
||||
Mocker.GetMock<IUpgradableSpecification>()
|
||||
.Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
|
||||
.Returns(true);
|
||||
|
||||
|
@ -170,7 +170,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
GivenExistingFile(new QualityModel(Quality.HDTV720p));
|
||||
GivenUpgradeForExistingFile();
|
||||
|
||||
Mocker.GetMock<IQualityUpgradableSpecification>()
|
||||
Mocker.GetMock<IUpgradableSpecification>()
|
||||
.Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
|
||||
.Returns(true);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ using NzbDrone.Core.Parser.Model;
|
|||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
|
@ -28,7 +28,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Mocker.Resolve<QualityUpgradableSpecification>();
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
|
||||
_firstFile = new MovieFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)), DateAdded = DateTime.Now };
|
||||
_secondFile = new MovieFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)), DateAdded = DateTime.Now };
|
||||
|
|
|
@ -9,8 +9,6 @@ using NzbDrone.Core.Parser.Model;
|
|||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
|
@ -27,7 +25,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Mocker.Resolve<QualityUpgradableSpecification>();
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
_upgradeDisk = Mocker.Resolve<UpgradeDiskSpecification>();
|
||||
|
||||
_firstFile = new MovieFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), DateAdded = DateTime.Now };
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(155)]
|
||||
public class add_update_allowed_quality_profile : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Profiles").AddColumn("UpgradeAllowed").AsInt32().Nullable();
|
||||
|
||||
// Set upgrade allowed for existing profiles (default will be false for new profiles)
|
||||
Update.Table("Profiles").Set(new { UpgradeAllowed = true }).AllRows();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,15 +12,16 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
{
|
||||
public class DownloadDecisionComparer : IComparer<DownloadDecision>
|
||||
{
|
||||
private readonly IDelayProfileService _delayProfileService;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IDelayProfileService _delayProfileService;
|
||||
|
||||
public delegate int CompareDelegate(DownloadDecision x, DownloadDecision y);
|
||||
public delegate int CompareDelegate<TSubject, TValue>(DownloadDecision x, DownloadDecision y);
|
||||
|
||||
public DownloadDecisionComparer(IDelayProfileService delayProfileService, IConfigService configService)
|
||||
public DownloadDecisionComparer(IConfigService configService, IDelayProfileService delayProfileService)
|
||||
{
|
||||
_delayProfileService = delayProfileService;
|
||||
_configService = configService;
|
||||
_delayProfileService = delayProfileService;
|
||||
}
|
||||
|
||||
public int Compare(DownloadDecision x, DownloadDecision y)
|
||||
|
|
|
@ -11,6 +11,7 @@ using NzbDrone.Core.Parser;
|
|||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine
|
||||
{
|
||||
|
@ -24,7 +25,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
{
|
||||
private readonly IEnumerable<IDecisionEngineSpecification> _specifications;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IQualityDefinitionService _definitionService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
|
@ -34,7 +35,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
{
|
||||
_specifications = specifications;
|
||||
_parsingService = parsingService;
|
||||
_configService = configService;
|
||||
_configService = configService;
|
||||
_definitionService = qualityDefinitionService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
return decisions.Where(c => c.RemoteMovie.MappingResult == MappingResultType.Success || c.RemoteMovie.MappingResult == MappingResultType.SuccessLenientMapping)
|
||||
.GroupBy(c => c.RemoteMovie.Movie.Id, (movieId, downloadDecisions) =>
|
||||
{
|
||||
return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_delayProfileService, _configService));
|
||||
return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_configService, _delayProfileService));
|
||||
})
|
||||
.SelectMany(c => c)
|
||||
.Union(decisions.Where(c => c.RemoteMovie.MappingResult != MappingResultType.Success || c.RemoteMovie.MappingResult != MappingResultType.SuccessLenientMapping))
|
||||
|
|
10
src/NzbDrone.Core/DecisionEngine/SpecificationPriority.cs
Normal file
10
src/NzbDrone.Core/DecisionEngine/SpecificationPriority.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace NzbDrone.Core.DecisionEngine
|
||||
{
|
||||
public enum SpecificationPriority
|
||||
{
|
||||
Default = 0,
|
||||
Parsing = 0,
|
||||
Database = 0,
|
||||
Disk = 1
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class AlreadyImportedSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AlreadyImportedSpecification(IHistoryService historyService,
|
||||
IConfigService configService,
|
||||
Logger logger)
|
||||
{
|
||||
_historyService = historyService;
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Database;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var cdhEnabled = _configService.EnableCompletedDownloadHandling;
|
||||
|
||||
if (!cdhEnabled)
|
||||
{
|
||||
_logger.Debug("Skipping already imported check because CDH is disabled");
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
var movie = subject.Movie;
|
||||
|
||||
_logger.Debug("Performing already imported check on report");
|
||||
if (movie != null)
|
||||
{
|
||||
if (!movie.HasFile)
|
||||
{
|
||||
_logger.Debug("Skipping already imported check for episode without file");
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
var historyForEpisode = _historyService.GetByMovieId(movie.Id, null);
|
||||
var lastGrabbed = historyForEpisode.FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed);
|
||||
|
||||
if (lastGrabbed == null)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
var imported = historyForEpisode.FirstOrDefault(h =>
|
||||
h.EventType == HistoryEventType.DownloadFolderImported &&
|
||||
h.DownloadId == lastGrabbed.DownloadId);
|
||||
|
||||
if (imported == null)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
// This is really only a guard against redownloading the same release over
|
||||
// and over when the grabbed and imported qualities do not match, if they do
|
||||
// match skip this check.
|
||||
if (lastGrabbed.Quality.Equals(imported.Quality))
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
var release = subject.Release;
|
||||
|
||||
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
var torrentInfo = release as TorrentInfo;
|
||||
|
||||
if (torrentInfo?.InfoHash != null && torrentInfo.InfoHash.ToUpper() == lastGrabbed.DownloadId)
|
||||
{
|
||||
_logger.Debug("Has same torrent hash as a grabbed and imported release");
|
||||
return Decision.Reject("Has same torrent hash as a grabbed and imported release");
|
||||
}
|
||||
}
|
||||
|
||||
// Only based on title because a release with the same title on another indexer/released at
|
||||
// a different time very likely has the exact same content and we don't need to also try it.
|
||||
|
||||
if (release.Title.Equals(lastGrabbed.SourceTitle, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.Debug("Has same release name as a grabbed and imported release");
|
||||
return Decision.Reject("Has same release name as a grabbed and imported release");
|
||||
}
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Movies;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class AnimeVersionUpgradeSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AnimeVersionUpgradeSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, Logger logger)
|
||||
{
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ using NLog;
|
|||
using NzbDrone.Core.Blacklisting;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
|
@ -17,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Database;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class BlockedIndexerSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IIndexerStatusService _indexerStatusService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly ICachedDictionary<IndexerStatus> _blockedIndexerCache;
|
||||
|
||||
public BlockedIndexerSpecification(IIndexerStatusService indexerStatusService, ICacheManager cacheManager, Logger logger)
|
||||
{
|
||||
_indexerStatusService = indexerStatusService;
|
||||
_logger = logger;
|
||||
|
||||
_blockedIndexerCache = cacheManager.GetCacheDictionary(GetType(), "blocked", FetchBlockedIndexer, TimeSpan.FromSeconds(15));
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Database;
|
||||
public RejectionType Type => RejectionType.Temporary;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var status = _blockedIndexerCache.Find(subject.Release.IndexerId.ToString());
|
||||
if (status != null)
|
||||
{
|
||||
return Decision.Reject($"Indexer {subject.Release.Indexer} is blocked till {status.DisabledTill} due to failures, cannot grab release.");
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
private IDictionary<string, IndexerStatus> FetchBlockedIndexer()
|
||||
{
|
||||
return _indexerStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId.ToString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -8,15 +8,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
{
|
||||
public class CutoffSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly UpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public CutoffSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, Logger logger)
|
||||
public CutoffSpecification(UpgradableSpecification qualityUpgradableSpecification, Logger logger)
|
||||
{
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public interface IDecisionEngineSpecification
|
||||
{
|
||||
RejectionType Type { get; }
|
||||
|
||||
|
||||
SpecificationPriority Priority { get; }
|
||||
|
||||
Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -17,6 +17,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
@ -16,6 +17,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Temporary;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
@ -28,6 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
|
||||
var age = subject.Release.AgeMinutes;
|
||||
var minimumAge = _configService.MinimumAge;
|
||||
var ageRounded = Math.Round(age, 1);
|
||||
|
||||
if (minimumAge == 0)
|
||||
{
|
||||
|
@ -36,15 +39,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
}
|
||||
|
||||
|
||||
_logger.Debug("Checking if report meets minimum age requirements. {0}", age);
|
||||
_logger.Debug("Checking if report meets minimum age requirements. {0}", ageRounded);
|
||||
|
||||
if (age < minimumAge)
|
||||
{
|
||||
_logger.Debug("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge);
|
||||
return Decision.Reject("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge);
|
||||
_logger.Debug("Only {0} minutes old, minimum age is {1} minutes", ageRounded, minimumAge);
|
||||
return Decision.Reject("Only {0} minutes old, minimum age is {1} minutes", ageRounded, minimumAge);
|
||||
}
|
||||
|
||||
_logger.Debug("Release is {0} minutes old, greater than minimum age of {1} minutes", age, minimumAge);
|
||||
_logger.Debug("Release is {0} minutes old, greater than minimum age of {1} minutes", ageRounded, minimumAge);
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
@ -9,6 +8,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public NotSampleSpecification(Logger logger)
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -13,11 +13,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
_logger.Debug("Checking if report meets quality requirements. {0}", subject.ParsedMovieInfo.Quality);
|
||||
|
||||
var profile = subject.Movie.Profile.Value;
|
||||
var qualityIndex = profile.GetIndex(subject.ParsedMovieInfo.Quality.Quality);
|
||||
var qualityOrGroup = profile.Items[qualityIndex.Index];
|
||||
|
|
|
@ -9,11 +9,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
public class QueueSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IQueueService _queueService;
|
||||
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly UpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public QueueSpecification(IQueueService queueService,
|
||||
QualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
UpgradableSpecification qualityUpgradableSpecification,
|
||||
Logger logger)
|
||||
{
|
||||
_queueService = queueService;
|
||||
|
@ -21,6 +21,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
@ -8,8 +9,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
{
|
||||
public class RawDiskSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private static readonly string[] _dvdContainerTypes = new[] { "vob", "iso" };
|
||||
private static readonly Regex[] DiscRegex = new[]
|
||||
{
|
||||
new Regex(@"(?:dis[ck])(?:[-_. ]\d+[-_. ])(?:(?:(?:480|720|1080|2160)[ip]|)[-_. ])?(?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?:(?:480|720|1080|2160)[ip]|)[-_. ](?:full)[-_. ](?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
private static readonly string[] _dvdContainerTypes = new[] { "vob", "iso" };
|
||||
private static readonly string[] _blurayContainerTypes = new[] { "m2ts" };
|
||||
|
||||
private readonly Logger _logger;
|
||||
|
@ -19,11 +25,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (subject.Release == null || subject.Release.Container.IsNullOrWhiteSpace())
|
||||
if (subject.Release == null)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
foreach (var regex in DiscRegex)
|
||||
{
|
||||
if (regex.IsMatch(subject.Release.Title))
|
||||
{
|
||||
_logger.Debug("Release contains raw Bluray, rejecting.");
|
||||
return Decision.Reject("Raw Bluray release");
|
||||
}
|
||||
}
|
||||
|
||||
if (subject.Release.Container.IsNullOrWhiteSpace())
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
//public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -19,6 +19,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -11,12 +11,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
public class DelaySpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IPendingReleaseService _pendingReleaseService;
|
||||
private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly IDelayProfileService _delayProfileService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public DelaySpecification(IPendingReleaseService pendingReleaseService,
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
IDelayProfileService delayProfileService,
|
||||
Logger logger)
|
||||
{
|
||||
|
@ -26,6 +26,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Database;
|
||||
public RejectionType Type => RejectionType.Temporary;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -11,12 +11,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
public class HistorySpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly UpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public HistorySpecification(IHistoryService historyService,
|
||||
QualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
UpgradableSpecification qualityUpgradableSpecification,
|
||||
IConfigService configService,
|
||||
Logger logger)
|
||||
{
|
||||
|
@ -26,6 +26,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Database;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -15,6 +15,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -9,17 +9,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
{
|
||||
public class ProperSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly UpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ProperSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, IConfigService configService, Logger logger)
|
||||
public ProperSpecification(UpgradableSpecification qualityUpgradableSpecification, IConfigService configService, Logger logger)
|
||||
{
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Movies;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
|
||||
{
|
||||
public class DailyEpisodeMatchSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public DailyEpisodeMatchSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
@ -17,29 +17,32 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
//public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var torrentInfo = subject.Release as TorrentInfo;
|
||||
|
||||
IIndexerSettings indexerSettings = null;
|
||||
try {
|
||||
indexerSettings = _indexerFactory.Get(subject.Release.IndexerId)?.Settings as IIndexerSettings;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Debug("Indexer with id {0} does not exist, skipping minimum seeder checks.", subject.Release.IndexerId);
|
||||
}
|
||||
|
||||
|
||||
if (torrentInfo == null || indexerSettings == null)
|
||||
if (torrentInfo == null || torrentInfo.IndexerId == 0)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
if (indexerSettings is ITorrentIndexerSettings torrentIndexerSettings)
|
||||
IndexerDefinition indexer;
|
||||
try
|
||||
{
|
||||
indexer = _indexerFactory.Get(torrentInfo.IndexerId);
|
||||
}
|
||||
catch (ModelNotFoundException)
|
||||
{
|
||||
_logger.Debug("Indexer with id {0} does not exist, skipping seeders check", torrentInfo.IndexerId);
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings;
|
||||
|
||||
if (torrentIndexerSettings != null)
|
||||
{
|
||||
var minimumSeeders = torrentIndexerSettings.MinimumSeeders;
|
||||
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public interface IQualityUpgradableSpecification
|
||||
public interface IUpgradableSpecification
|
||||
{
|
||||
bool IsUpgradable(Profile profile, QualityModel currentQuality, QualityModel newQuality = null);
|
||||
bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null);
|
||||
bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality);
|
||||
bool IsUpgradeAllowed(Profile qualityProfile, QualityModel currentQuality, QualityModel newQuality);
|
||||
}
|
||||
|
||||
public class QualityUpgradableSpecification : IQualityUpgradableSpecification
|
||||
public class UpgradableSpecification : IUpgradableSpecification
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public QualityUpgradableSpecification(Logger logger)
|
||||
public UpgradableSpecification(IConfigService configService, Logger logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
@ -75,5 +79,18 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsUpgradeAllowed(Profile qualityProfile, QualityModel currentQuality, QualityModel newQuality)
|
||||
{
|
||||
var isQualityUpgrade = new QualityModelComparer(qualityProfile).Compare(newQuality, currentQuality) > 0;
|
||||
|
||||
if (isQualityUpgrade && qualityProfile.UpgradeAllowed)
|
||||
{
|
||||
_logger.Debug("Quality profile allows upgrading");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class UpgradeAllowedSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly UpgradableSpecification _upgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public UpgradeAllowedSpecification(UpgradableSpecification upgradableSpecification, Logger logger)
|
||||
{
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var qualityProfile = subject.Movie.Profile.Value;
|
||||
|
||||
if (subject.Movie.MovieFileId != 0)
|
||||
{
|
||||
var file = subject.Movie.MovieFile;
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
_logger.Debug("File is no longer available, skipping this file.");
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
_logger.Debug("Comparing file quality with report. Existing file is {0}", file.Quality);
|
||||
|
||||
if (!_upgradableSpecification.IsUpgradeAllowed(qualityProfile,
|
||||
file.Quality,
|
||||
subject.ParsedMovieInfo.Quality))
|
||||
{
|
||||
_logger.Debug("Upgrading is not allowed by the quality profile");
|
||||
|
||||
return Decision.Reject("Existing file and the Quality profile does not allow upgrades");
|
||||
}
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,15 +7,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
{
|
||||
public class UpgradeDiskSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly UpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public UpgradeDiskSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, Logger logger)
|
||||
public UpgradeDiskSpecification(UpgradableSpecification qualityUpgradableSpecification, Logger logger)
|
||||
{
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Marr.Data;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
|
@ -7,6 +7,7 @@ using NzbDrone.Core.Qualities;
|
|||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
|
@ -20,6 +21,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
public string SceneName { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public MediaInfoModel MediaInfo { get; set; }
|
||||
public string Edition { get; set; }
|
||||
public LazyLoaded<Movie> Movie { get; set; }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
||||
|
@ -9,9 +10,8 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
{
|
||||
public string Path { get; set; }
|
||||
public string FolderName { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public List<int> EpisodeIds { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public int MovieId { get; set; }
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
||||
{
|
||||
|
@ -9,9 +10,11 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
{
|
||||
public string Path { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public string FolderName { get; set; }
|
||||
public string Name { get; set; }
|
||||
public long Size { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
public Movie Movie { get; set; }
|
||||
|
|
|
@ -127,7 +127,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
var movieFiles = _diskScanService.GetVideoFiles(folder).ToList();
|
||||
var decisions = _importDecisionMaker.GetImportDecisions(movieFiles, movie, downloadClientItem, folderInfo, SceneSource(movie, folder), false);
|
||||
|
||||
return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList();
|
||||
return decisions.Select(decision => MapItem(decision, folder, downloadId, directoryInfo.Name)).ToList();
|
||||
}
|
||||
|
||||
private ManualImportItem ProcessFile(string file, string downloadId, string folder = null)
|
||||
|
@ -167,23 +167,28 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
Size = _diskProvider.GetFileSize(file)
|
||||
};
|
||||
|
||||
return MapItem(new ImportDecision(localMovie, new Rejection("Unknown Movie")), folder, downloadId);
|
||||
return MapItem(new ImportDecision(localMovie, new Rejection("Unknown Movie")), folder, downloadId, null);
|
||||
}
|
||||
|
||||
var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> { file },
|
||||
movie, downloadClientItem, null, SceneSource(movie, folder), true);
|
||||
|
||||
return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : new ManualImportItem
|
||||
{
|
||||
DownloadId = downloadId,
|
||||
Path = file,
|
||||
RelativePath = folder.GetRelativePath(file),
|
||||
Name = Path.GetFileNameWithoutExtension(file),
|
||||
Rejections = new List<Rejection>
|
||||
{
|
||||
new Rejection("Unable to process file")
|
||||
}
|
||||
};
|
||||
if (importDecisions.Any())
|
||||
{
|
||||
return MapItem(importDecisions.First(), folder, downloadId, null);
|
||||
}
|
||||
|
||||
return new ManualImportItem
|
||||
{
|
||||
DownloadId = downloadId,
|
||||
Path = file,
|
||||
RelativePath = folder.GetRelativePath(file),
|
||||
Name = Path.GetFileNameWithoutExtension(file),
|
||||
Rejections = new List<Rejection>
|
||||
{
|
||||
new Rejection("Unable to process file")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private bool SceneSource(Movie movie, string folder)
|
||||
|
@ -191,12 +196,13 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
return !(movie.Path.PathEquals(folder) || movie.Path.IsParentPath(folder));
|
||||
}
|
||||
|
||||
private ManualImportItem MapItem(ImportDecision decision, string folder, string downloadId)
|
||||
private ManualImportItem MapItem(ImportDecision decision, string rootFolder, string downloadId, string folderName)
|
||||
{
|
||||
var item = new ManualImportItem();
|
||||
|
||||
item.Path = decision.LocalMovie.Path;
|
||||
item.RelativePath = folder.GetRelativePath(decision.LocalMovie.Path);
|
||||
item.FolderName = folderName;
|
||||
item.RelativePath = rootFolder.GetRelativePath(decision.LocalMovie.Path);
|
||||
item.Name = Path.GetFileNameWithoutExtension(decision.LocalMovie.Path);
|
||||
item.DownloadId = downloadId;
|
||||
|
||||
|
@ -207,6 +213,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
|
||||
item.Quality = decision.LocalMovie.Quality;
|
||||
item.Size = _diskProvider.GetFileSize(decision.LocalMovie.Path);
|
||||
item.Languages = decision.LocalMovie.Languages;
|
||||
item.Rejections = decision.Rejections;
|
||||
|
||||
return item;
|
||||
|
@ -236,6 +243,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
ParsedMovieInfo = parsedMovieInfo,
|
||||
Path = file.Path,
|
||||
Quality = file.Quality,
|
||||
Languages = file.Languages,
|
||||
Movie = movie,
|
||||
Size = 0
|
||||
};
|
||||
|
@ -248,7 +256,6 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
{
|
||||
imported.AddRange(_importApprovedMovie.Import(new List<ImportDecision> { importDecision }, !existingFile, null, message.ImportMode));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
var trackedDownload = _trackedDownloadService.Find(file.DownloadId);
|
||||
|
|
|
@ -155,9 +155,15 @@
|
|||
<Compile Include="Datastore\Migration\152_add_custom_filters.cs" />
|
||||
<Compile Include="Datastore\Migration\153_indexer_client_status_search_changes.cs" />
|
||||
<Compile Include="Datastore\Migration\154_add_language_to_file_history_blacklist.cs" />
|
||||
<Compile Include="Datastore\Migration\155_add_update_allowed_quality_profile.cs" />
|
||||
<Compile Include="DecisionEngine\SpecificationPriority.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\AlreadyImportedSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\BlockedIndexerSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\CustomFormatAllowedByProfileSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\IDecisionEngineSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\MaximumSizeSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\RequiredIndexerFlagsSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\UpgradeAllowedSpecification.cs" />
|
||||
<Compile Include="Download\Clients\DownloadClientUnavailableException.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxySelector.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxyV1.cs" />
|
||||
|
@ -440,14 +446,12 @@
|
|||
<Compile Include="DecisionEngine\DownloadDecisionComparer.cs" />
|
||||
<Compile Include="DecisionEngine\DownloadDecisionMaker.cs" />
|
||||
<Compile Include="DecisionEngine\DownloadDecisionPriorizationService.cs" />
|
||||
<Compile Include="DecisionEngine\IDecisionEngineSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\IRejectWithReason.cs" />
|
||||
<Compile Include="DecisionEngine\QualityUpgradableSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\UpgradableSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Rejection.cs" />
|
||||
<Compile Include="DecisionEngine\RejectionType.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\AcceptableSizeSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\BlacklistSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\AnimeVersionUpgradeSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\CutoffSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\ProtocolSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\LanguageSpecification.cs" />
|
||||
|
@ -462,7 +466,6 @@
|
|||
<Compile Include="DecisionEngine\Specifications\RssSync\MonitoredMovieSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\RssSync\AvailabilitySpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\RssSync\ProperSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\Search\DailyEpisodeMatchSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\Search\MovieSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\TorrentSeedingSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\RawDiskSpecification.cs" />
|
||||
|
@ -1266,7 +1269,6 @@
|
|||
<Compile Include="Update\UpdateVerificationFailedException.cs" />
|
||||
<Compile Include="Validation\FolderValidator.cs" />
|
||||
<Compile Include="Validation\IpValidation.cs" />
|
||||
<Compile Include="Validation\LanguageValidator.cs" />
|
||||
<Compile Include="Validation\NzbDroneValidationExtensions.cs" />
|
||||
<Compile Include="Validation\NzbDroneValidationFailure.cs" />
|
||||
<Compile Include="Validation\NzbDroneValidationResult.cs" />
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
||||
namespace NzbDrone.Core.Parser.Model
|
||||
{
|
||||
|
@ -17,6 +18,7 @@ namespace NzbDrone.Core.Parser.Model
|
|||
public ParsedMovieInfo ParsedMovieInfo { get; set; }
|
||||
public Movie Movie { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public MediaInfoModel MediaInfo { get; set; }
|
||||
public bool ExistingFile { get; set; }
|
||||
|
||||
|
@ -26,4 +28,4 @@ namespace NzbDrone.Core.Parser.Model
|
|||
return Path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace NzbDrone.Core.Profiles
|
|||
public List<ProfileFormatItem> FormatItems { get; set; }
|
||||
public List<string> PreferredTags { get; set; }
|
||||
public Language Language { get; set; }
|
||||
public bool UpgradeAllowed { get; set; }
|
||||
|
||||
public Quality LastAllowedQuality()
|
||||
{
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
using FluentValidation.Validators;
|
||||
|
||||
namespace NzbDrone.Core.Validation
|
||||
{
|
||||
public class LanguageValidator : PropertyValidator
|
||||
{
|
||||
public LanguageValidator()
|
||||
: base("Unknown Language")
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue == null) return false;
|
||||
|
||||
if ((int) context.PropertyValue == 0) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -58,11 +58,6 @@ namespace NzbDrone.Core.Validation
|
|||
});
|
||||
}
|
||||
|
||||
public static IRuleBuilderOptions<T, Language> ValidLanguage<T>(this IRuleBuilder<T, Language> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.SetValidator(new LanguageValidator());
|
||||
}
|
||||
|
||||
public static IRuleBuilderOptions<T, TProp> AsWarning<T, TProp>(this IRuleBuilderOptions<T, TProp> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.WithState(v => NzbDroneValidationState.Warning);
|
||||
|
|
43
src/Radarr.Api.V2/ManualImport/ManualImportModule.cs
Normal file
43
src/Radarr.Api.V2/ManualImport/ManualImportModule.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.MediaFiles.MovieImport.Manual;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.Extensions;
|
||||
|
||||
namespace Radarr.Api.V2.ManualImport
|
||||
{
|
||||
public class ManualImportModule : RadarrRestModule<ManualImportResource>
|
||||
{
|
||||
private readonly IManualImportService _manualImportService;
|
||||
|
||||
public ManualImportModule(IManualImportService manualImportService)
|
||||
: base("/manualimport")
|
||||
{
|
||||
_manualImportService = manualImportService;
|
||||
|
||||
GetResourceAll = GetMediaFiles;
|
||||
}
|
||||
|
||||
private List<ManualImportResource> GetMediaFiles()
|
||||
{
|
||||
var folder = (string)Request.Query.folder;
|
||||
var downloadId = (string)Request.Query.downloadId;
|
||||
var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true);
|
||||
|
||||
return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList();
|
||||
}
|
||||
|
||||
private ManualImportResource AddQualityWeight(ManualImportResource item)
|
||||
{
|
||||
if (item.Quality != null)
|
||||
{
|
||||
item.QualityWeight = Quality.DefaultQualityDefinitions.Single(q => q.Quality == item.Quality.Quality).Weight;
|
||||
item.QualityWeight += item.Quality.Revision.Real * 10;
|
||||
item.QualityWeight += item.Quality.Revision.Version;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
56
src/Radarr.Api.V2/ManualImport/ManualImportResource.cs
Normal file
56
src/Radarr.Api.V2/ManualImport/ManualImportResource.cs
Normal file
|
@ -0,0 +1,56 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Crypto;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.MediaFiles.MovieImport.Manual;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Radarr.Api.V2.Movies;
|
||||
using Radarr.Http.REST;
|
||||
|
||||
namespace Radarr.Api.V2.ManualImport
|
||||
{
|
||||
public class ManualImportResource : RestResource
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public string FolderName { get; set; }
|
||||
public string Name { get; set; }
|
||||
public long Size { get; set; }
|
||||
public MovieResource Movie { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public int QualityWeight { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
}
|
||||
|
||||
public static class ManualImportResourceMapper
|
||||
{
|
||||
public static ManualImportResource ToResource(this ManualImportItem model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new ManualImportResource
|
||||
{
|
||||
Id = HashConverter.GetHashInt31(model.Path),
|
||||
Path = model.Path,
|
||||
RelativePath = model.RelativePath,
|
||||
// FolderName = model.FolderName,
|
||||
Name = model.Name,
|
||||
Size = model.Size,
|
||||
Movie = model.Movie.ToResource(),
|
||||
Quality = model.Quality,
|
||||
Languages = model.Languages,
|
||||
//QualityWeight
|
||||
DownloadId = model.DownloadId,
|
||||
Rejections = model.Rejections
|
||||
};
|
||||
}
|
||||
|
||||
public static List<ManualImportResource> ToResource(this IEnumerable<ManualImportItem> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ using System.Linq;
|
|||
using Nancy;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
|
@ -25,13 +25,13 @@ namespace Radarr.Api.V2.MovieFiles
|
|||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly IRecycleBinProvider _recycleBinProvider;
|
||||
private readonly IMovieService _movieService;
|
||||
private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public MovieFileModule(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IMediaFileService mediaFileService,
|
||||
IMovieService movieService,
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
Logger logger)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Radarr.Http.REST;
|
||||
|
@ -17,8 +19,8 @@ namespace Radarr.Api.V2.MovieFiles
|
|||
public string SceneName { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public MediaInfoResource MediaInfo { get; set; }
|
||||
|
||||
public bool QualityCutoffNotMet { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
}
|
||||
|
||||
public static class MovieFileResourceMapper
|
||||
|
@ -38,6 +40,7 @@ namespace Radarr.Api.V2.MovieFiles
|
|||
DateAdded = model.DateAdded,
|
||||
SceneName = model.SceneName,
|
||||
Quality = model.Quality,
|
||||
Languages = model.Languages,
|
||||
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
||||
//QualityCutoffNotMet
|
||||
};
|
||||
|
@ -59,12 +62,13 @@ namespace Radarr.Api.V2.MovieFiles
|
|||
DateAdded = model.DateAdded,
|
||||
SceneName = model.SceneName,
|
||||
Quality = model.Quality,
|
||||
Languages = model.Languages,
|
||||
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
||||
// QualityCutoffNotMet = upgradableSpecification.CutoffNotMet(movie.Profile.Value, model.Quality)
|
||||
};
|
||||
}
|
||||
|
||||
public static MovieFileResource ToResource(this MovieFile model, NzbDrone.Core.Movies.Movie movie, IQualityUpgradableSpecification upgradableSpecification)
|
||||
public static MovieFileResource ToResource(this MovieFile model, NzbDrone.Core.Movies.Movie movie, IUpgradableSpecification upgradableSpecification)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
|
@ -79,6 +83,7 @@ namespace Radarr.Api.V2.MovieFiles
|
|||
DateAdded = model.DateAdded,
|
||||
SceneName = model.SceneName,
|
||||
Quality = model.Quality,
|
||||
Languages = model.Languages,
|
||||
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
||||
QualityCutoffNotMet = upgradableSpecification.CutoffNotMet(movie.Profile.Value, model.Quality)
|
||||
};
|
||||
|
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser;
|
||||
using Radarr.Http;
|
||||
|
||||
namespace Radarr.Api.V2.Profiles.Languages
|
||||
|
@ -28,12 +27,11 @@ namespace Radarr.Api.V2.Profiles.Languages
|
|||
|
||||
private List<LanguageResource> GetAll()
|
||||
{
|
||||
return ((Language[])Enum.GetValues(typeof (Language)))
|
||||
.Select(l => new LanguageResource
|
||||
{
|
||||
Id = (int) l,
|
||||
Name = l.ToString()
|
||||
})
|
||||
return Language.All.Select(l => new LanguageResource
|
||||
{
|
||||
Id = (int)l,
|
||||
Name = l.ToString()
|
||||
})
|
||||
.OrderBy(l => l.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||
ruleBuilder.SetValidator(new AllowedValidator<T>());
|
||||
ruleBuilder.SetValidator(new QualityNameValidator<T>());
|
||||
ruleBuilder.SetValidator(new EmptyItemGroupNameValidator<T>());
|
||||
ruleBuilder.SetValidator(new GroupItemValidator<T>());
|
||||
ruleBuilder.SetValidator(new ItemGroupIdValidator<T>());
|
||||
ruleBuilder.SetValidator(new UniqueIdValidator<T>());
|
||||
ruleBuilder.SetValidator(new UniqueQualityIdValidator<T>());
|
||||
|
@ -47,10 +47,10 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
}
|
||||
}
|
||||
|
||||
public class EmptyItemGroupNameValidator<T> : PropertyValidator
|
||||
public class GroupItemValidator<T> : PropertyValidator
|
||||
{
|
||||
public EmptyItemGroupNameValidator()
|
||||
: base("Groups must not be empty")
|
||||
public GroupItemValidator()
|
||||
: base("Groups must contain multiple qualities")
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Empty()))
|
||||
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Count <= 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
// TODO: Need to validate the Items to ensure groups have names and at no item has no name, no items and no quality
|
||||
SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff();
|
||||
SharedValidator.RuleFor(c => c.Items).ValidItems();
|
||||
SharedValidator.RuleFor(c => c.Language).ValidLanguage();
|
||||
SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
|
||||
{
|
||||
var all = _formatService.All().Select(f => f.Id).ToList();
|
||||
|
@ -46,8 +45,8 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
private int Create(QualityProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
return _profileService.Add(model).Id;
|
||||
model = _profileService.Add(model);
|
||||
return model.Id;
|
||||
}
|
||||
|
||||
private void DeleteProfile(int id)
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
public class QualityProfileResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool UpgradeAllowed { get; set; }
|
||||
public int Cutoff { get; set; }
|
||||
public string PreferredTags { get; set; }
|
||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||
|
@ -49,6 +50,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
UpgradeAllowed = model.UpgradeAllowed,
|
||||
Cutoff = model.Cutoff,
|
||||
PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "",
|
||||
Items = model.Items.ConvertAll(ToResource),
|
||||
|
@ -89,6 +91,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
UpgradeAllowed = resource.UpgradeAllowed,
|
||||
Cutoff = resource.Cutoff,
|
||||
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
|
|
|
@ -123,6 +123,8 @@
|
|||
<Compile Include="Logs\LogModule.cs" />
|
||||
<Compile Include="Logs\LogResource.cs" />
|
||||
<Compile Include="Logs\UpdateLogFileModule.cs" />
|
||||
<Compile Include="ManualImport\ManualImportModule.cs" />
|
||||
<Compile Include="ManualImport\ManualImportResource.cs" />
|
||||
<Compile Include="Metadata\MetadataModule.cs" />
|
||||
<Compile Include="Metadata\MetadataResource.cs" />
|
||||
<Compile Include="MovieFiles\MediaInfoResource.cs" />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue