diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModal.js b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModal.js
deleted file mode 100644
index 35d00caf2..000000000
--- a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModal.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import Modal from 'Components/Modal/Modal';
-import EpisodeFileEditorModalContentConnector from './EpisodeFileEditorModalContentConnector';
-
-function EpisodeFileEditorModal(props) {
- const {
- isOpen,
- onModalClose,
- ...otherProps
- } = props;
-
- return (
-
- {
- isOpen &&
-
- }
-
- );
-}
-
-EpisodeFileEditorModal.propTypes = {
- isOpen: PropTypes.bool.isRequired,
- onModalClose: PropTypes.func.isRequired
-};
-
-export default EpisodeFileEditorModal;
diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.css b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.css
deleted file mode 100644
index 49e946826..000000000
--- a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.actions {
- display: flex;
- margin-right: auto;
-}
-
-.selectInput {
- margin-left: 10px;
-}
diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.js b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.js
deleted file mode 100644
index 59602f682..000000000
--- a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.js
+++ /dev/null
@@ -1,310 +0,0 @@
-import _ from 'lodash';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
-import getSelectedIds from 'Utilities/Table/getSelectedIds';
-import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
-import selectAll from 'Utilities/Table/selectAll';
-import toggleSelected from 'Utilities/Table/toggleSelected';
-import { kinds } from 'Helpers/Props';
-import SelectInput from 'Components/Form/SelectInput';
-import Button from 'Components/Link/Button';
-import SpinnerButton from 'Components/Link/SpinnerButton';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import ConfirmModal from 'Components/Modal/ConfirmModal';
-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 Table from 'Components/Table/Table';
-import TableBody from 'Components/Table/TableBody';
-import SeasonNumber from 'Season/SeasonNumber';
-import EpisodeFileEditorRow from './EpisodeFileEditorRow';
-import styles from './EpisodeFileEditorModalContent.css';
-
-const columns = [
- {
- name: 'episodeNumber',
- label: 'Episode',
- isVisible: true
- },
- {
- name: 'relativePath',
- label: 'Relative Path',
- isVisible: true
- },
- {
- name: 'airDateUtc',
- label: 'Air Date',
- isVisible: true
- },
- {
- name: 'language',
- label: 'Language',
- isVisible: true
- },
- {
- name: 'quality',
- label: 'Quality',
- isVisible: true
- }
-];
-
-class EpisodeFileEditorModalContent extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- allSelected: false,
- allUnselected: false,
- lastToggled: null,
- selectedState: {},
- isConfirmDeleteModalOpen: false
- };
- }
-
- componentDidUpdate(prevProps) {
- if (hasDifferentItems(prevProps.items, this.props.items)) {
- this.setState((state) => {
- return removeOldSelectedState(state, prevProps.items);
- });
- }
- }
-
- //
- // Control
-
- getSelectedIds = () => {
- const selectedIds = getSelectedIds(this.state.selectedState);
-
- return selectedIds.reduce((acc, id) => {
- const matchingItem = this.props.items.find((item) => item.id === id);
-
- if (matchingItem && !acc.includes(matchingItem.episodeFileId)) {
- acc.push(matchingItem.episodeFileId);
- }
-
- return acc;
- }, []);
- }
-
- //
- // Listeners
-
- onSelectAllChange = ({ value }) => {
- this.setState(selectAll(this.state.selectedState, value));
- }
-
- onSelectedChange = ({ id, value, shiftKey = false }) => {
- this.setState((state) => {
- return toggleSelected(state, this.props.items, id, value, shiftKey);
- });
- }
-
- onDeletePress = () => {
- this.setState({ isConfirmDeleteModalOpen: true });
- }
-
- onConfirmDelete = () => {
- this.setState({ isConfirmDeleteModalOpen: false });
- this.props.onDeletePress(this.getSelectedIds());
- }
-
- onConfirmDeleteModalClose = () => {
- this.setState({ isConfirmDeleteModalOpen: false });
- }
-
- onLanguageChange = ({ value }) => {
- const selectedIds = this.getSelectedIds();
-
- if (!selectedIds.length) {
- return;
- }
-
- this.props.onLanguageChange(selectedIds, parseInt(value));
- }
-
- onQualityChange = ({ value }) => {
- const selectedIds = this.getSelectedIds();
-
- if (!selectedIds.length) {
- return;
- }
-
- this.props.onQualityChange(selectedIds, parseInt(value));
- }
-
- //
- // Render
-
- render() {
- const {
- seasonNumber,
- isDeleting,
- isFetching,
- isPopulated,
- error,
- items,
- languages,
- qualities,
- seriesType,
- onModalClose
- } = this.props;
-
- const {
- allSelected,
- allUnselected,
- selectedState,
- isConfirmDeleteModalOpen
- } = this.state;
-
- const languageOptions = _.reduceRight(languages, (acc, language) => {
- acc.push({
- key: language.id,
- value: language.name
- });
-
- return acc;
- }, [{ key: 'selectLanguage', value: 'Select Language', disabled: true }]);
-
- const qualityOptions = _.reduceRight(qualities, (acc, quality) => {
- acc.push({
- key: quality.id,
- value: quality.name
- });
-
- return acc;
- }, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]);
-
- const hasSelectedFiles = this.getSelectedIds().length > 0;
-
- return (
-
-
- Manage Episodes {seasonNumber != null && }
-
-
-
- {
- isFetching && !isPopulated ?
- :
- null
- }
-
- {
- !isFetching && error ?
- {error}
:
- null
- }
-
- {
- isPopulated && !items.length ?
-
- No episode files to manage.
-
:
- null
- }
-
- {
- isPopulated && items.length ?
-
-
- {
- items.map((item) => {
- return (
-
- );
- })
- }
-
-
:
- null
- }
-
-
-
-
-
- Delete
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-}
-
-EpisodeFileEditorModalContent.propTypes = {
- seasonNumber: PropTypes.number,
- isDeleting: PropTypes.bool.isRequired,
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- error: PropTypes.object,
- items: PropTypes.arrayOf(PropTypes.object).isRequired,
- languages: PropTypes.arrayOf(PropTypes.object).isRequired,
- qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
- seriesType: PropTypes.string.isRequired,
- onDeletePress: PropTypes.func.isRequired,
- onLanguageChange: PropTypes.func.isRequired,
- onQualityChange: PropTypes.func.isRequired,
- onModalClose: PropTypes.func.isRequired
-};
-
-export default EpisodeFileEditorModalContent;
diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContentConnector.js b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContentConnector.js
deleted file mode 100644
index 79c96d1c0..000000000
--- a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContentConnector.js
+++ /dev/null
@@ -1,174 +0,0 @@
-/* eslint max-params: 0 */
-import _ from 'lodash';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import getQualities from 'Utilities/Quality/getQualities';
-import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
-import { deleteEpisodeFiles, updateEpisodeFiles } from 'Store/Actions/episodeFileActions';
-import { fetchLanguageProfileSchema, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
-import EpisodeFileEditorModalContent from './EpisodeFileEditorModalContent';
-
-function createSchemaSelector() {
- return createSelector(
- (state) => state.settings.languageProfiles,
- (state) => state.settings.qualityProfiles,
- (languageProfiles, qualityProfiles) => {
- const languages = _.map(languageProfiles.schema.languages, 'language');
- const qualities = getQualities(qualityProfiles.schema.items);
-
- let error = null;
-
- if (languageProfiles.schemaError) {
- error = 'Unable to load languages';
- } else if (qualityProfiles.schemaError) {
- error = 'Unable to load qualities';
- }
-
- return {
- isFetching: languageProfiles.isSchemaFetching || qualityProfiles.isSchemaFetching,
- isPopulated: languageProfiles.isSchemaPopulated && qualityProfiles.isSchemaPopulated,
- error,
- languages,
- qualities
- };
- }
- );
-}
-
-function createMapStateToProps() {
- return createSelector(
- (state, { seasonNumber }) => seasonNumber,
- (state) => state.episodes,
- (state) => state.episodeFiles,
- createSchemaSelector(),
- createSeriesSelector(),
- (
- seasonNumber,
- episodes,
- episodeFiles,
- schema,
- series
- ) => {
- const filtered = _.filter(episodes.items, (episode) => {
- if (seasonNumber >= 0 && episode.seasonNumber !== seasonNumber) {
- return false;
- }
-
- if (!episode.episodeFileId) {
- return false;
- }
-
- return _.some(episodeFiles.items, { id: episode.episodeFileId });
- });
-
- const sorted = _.orderBy(filtered, ['seasonNumber', 'episodeNumber'], ['desc', 'desc']);
-
- const items = _.map(sorted, (episode) => {
- const episodeFile = _.find(episodeFiles.items, { id: episode.episodeFileId });
-
- return {
- relativePath: episodeFile.relativePath,
- language: episodeFile.language,
- quality: episodeFile.quality,
- languageCutoffNotMet: episodeFile.languageCutoffNotMet,
- qualityCutoffNotMet: episodeFile.qualityCutoffNotMet,
- ...episode
- };
- });
-
- return {
- ...schema,
- items,
- seriesType: series.seriesType,
- isDeleting: episodeFiles.isDeleting,
- isSaving: episodeFiles.isSaving
- };
- }
- );
-}
-
-function createMapDispatchToProps(dispatch, props) {
- return {
- dispatchFetchLanguageProfileSchema(name, path) {
- dispatch(fetchLanguageProfileSchema());
- },
-
- dispatchFetchQualityProfileSchema(name, path) {
- dispatch(fetchQualityProfileSchema());
- },
-
- dispatchUpdateEpisodeFiles(updateProps) {
- dispatch(updateEpisodeFiles(updateProps));
- },
-
- onDeletePress(episodeFileIds) {
- dispatch(deleteEpisodeFiles({ episodeFileIds }));
- }
- };
-}
-
-class EpisodeFileEditorModalContentConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidMount() {
- this.props.dispatchFetchLanguageProfileSchema();
- this.props.dispatchFetchQualityProfileSchema();
- }
-
- //
- // Listeners
-
- onLanguageChange = (episodeFileIds, languageId) => {
- const language = _.find(this.props.languages, { id: languageId });
-
- this.props.dispatchUpdateEpisodeFiles({ episodeFileIds, language });
- }
-
- onQualityChange = (episodeFileIds, qualityId) => {
- const quality = {
- quality: _.find(this.props.qualities, { id: qualityId }),
- revision: {
- version: 1,
- real: 0
- }
- };
-
- this.props.dispatchUpdateEpisodeFiles({ episodeFileIds, quality });
- }
-
- //
- // Render
-
- render() {
- const {
- dispatchFetchLanguageProfileSchema,
- dispatchFetchQualityProfileSchema,
- dispatchUpdateEpisodeFiles,
- ...otherProps
- } = this.props;
-
- return (
-
- );
- }
-}
-
-EpisodeFileEditorModalContentConnector.propTypes = {
- seriesId: PropTypes.number.isRequired,
- seasonNumber: PropTypes.number,
- languages: PropTypes.arrayOf(PropTypes.object).isRequired,
- qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
- dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired,
- dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
- dispatchUpdateEpisodeFiles: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeFileEditorModalContentConnector);
diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.css b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.css
deleted file mode 100644
index f86e1de6b..000000000
--- a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.absoluteEpisodeNumber {
- margin-left: 5px;
-}
diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.js b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.js
deleted file mode 100644
index 7e7acdc45..000000000
--- a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import padNumber from 'Utilities/Number/padNumber';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
-import TableRow from 'Components/Table/TableRow';
-import TableRowCell from 'Components/Table/Cells/TableRowCell';
-import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
-import EpisodeLanguage from 'Episode/EpisodeLanguage';
-import EpisodeQuality from 'Episode/EpisodeQuality';
-import styles from './EpisodeFileEditorRow';
-
-function EpisodeFileEditorRow(props) {
- const {
- id,
- seriesType,
- seasonNumber,
- episodeNumber,
- absoluteEpisodeNumber,
- relativePath,
- airDateUtc,
- language,
- quality,
- qualityCutoffNotMet,
- languageCutoffNotMet,
- isSelected,
- onSelectedChange
- } = props;
-
- return (
-
-
-
-
- {seasonNumber}x{padNumber(episodeNumber, 2)}
-
- {
- seriesType === 'anime' && !!absoluteEpisodeNumber &&
-
- ({absoluteEpisodeNumber})
-
- }
-
-
-
- {relativePath}
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-EpisodeFileEditorRow.propTypes = {
- id: PropTypes.number.isRequired,
- seriesType: PropTypes.string.isRequired,
- seasonNumber: PropTypes.number.isRequired,
- episodeNumber: PropTypes.number.isRequired,
- absoluteEpisodeNumber: PropTypes.number,
- relativePath: PropTypes.string.isRequired,
- airDateUtc: PropTypes.string.isRequired,
- language: PropTypes.object.isRequired,
- quality: PropTypes.object.isRequired,
- qualityCutoffNotMet: PropTypes.bool.isRequired,
- languageCutoffNotMet: PropTypes.bool.isRequired,
- isSelected: PropTypes.bool,
- onSelectedChange: PropTypes.func.isRequired
-};
-
-export default EpisodeFileEditorRow;
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css
index d50f3a261..5eb1aba72 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css
@@ -26,6 +26,12 @@
justify-content: flex-end;
}
+.deleteButton {
+ composes: button from '~Components/Link/Button.css';
+
+ margin-right: 10px;
+}
+
.importMode,
.bulkSelect {
composes: select from '~Components/Form/SelectInput.css';
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js
index 47077c163..ac98c70f5 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js
@@ -112,13 +112,21 @@ class InteractiveImportModalContent extends Component {
constructor(props, context) {
super(props, context);
+ const instanceColumns = _.cloneDeep(columns);
+
+ if (!props.showSeries) {
+ instanceColumns.find((c) => c.name === 'series').isVisible = false;
+ }
+
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
invalidRowsSelected: [],
- selectModalOpen: null
+ withoutEpisodeFileIdRowsSelected: [],
+ selectModalOpen: null,
+ columns: instanceColumns
};
}
@@ -136,9 +144,14 @@ class InteractiveImportModalContent extends Component {
this.setState(selectAll(this.state.selectedState, value));
}
- onSelectedChange = ({ id, value, shiftKey = false }) => {
+ onSelectedChange = ({ id, value, hasEpisodeFileId, shiftKey = false }) => {
this.setState((state) => {
- return toggleSelected(state, this.props.items, id, value, shiftKey);
+ return {
+ ...toggleSelected(state, this.props.items, id, value, shiftKey),
+ withoutEpisodeFileIdRowsSelected: hasEpisodeFileId || !value ?
+ _.without(state.withoutEpisodeFileIdRowsSelected, id) :
+ [...state.withoutEpisodeFileIdRowsSelected, id]
+ };
});
}
@@ -156,6 +169,16 @@ class InteractiveImportModalContent extends Component {
});
}
+ onDeleteSelectedPress = () => {
+ const {
+ onDeleteSelectedPress
+ } = this.props;
+
+ const selected = this.getSelectedIds();
+
+ onDeleteSelectedPress(selected);
+ }
+
onImportSelectedPress = () => {
const {
downloadId,
@@ -193,7 +216,9 @@ class InteractiveImportModalContent extends Component {
const {
downloadId,
allowSeriesChange,
+ autoSelectRow,
showFilterExistingFiles,
+ showDelete,
showImportMode,
filterExistingFiles,
title,
@@ -215,6 +240,7 @@ class InteractiveImportModalContent extends Component {
allUnselected,
selectedState,
invalidRowsSelected,
+ withoutEpisodeFileIdRowsSelected,
selectModalOpen
} = this.state;
@@ -308,7 +334,7 @@ class InteractiveImportModalContent extends Component {
{
isPopulated && !!items.length && !isFetching && !isFetching &&
@@ -345,6 +373,19 @@ class InteractiveImportModalContent extends Component {
+ {
+ showDelete ?
+
:
+ null
+ }
+
{
!downloadId && showImportMode ?
e.id);
+ const originalEpisodeIds = originalFile.episodes ? originalFile.episodes.map((e) => e.id) : [];
+
+ return episodeIds.every((episodeId) => {
+ return originalEpisodeIds.indexOf(episodeId) >= 0;
+ });
+}
+
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('interactiveImport'),
@@ -23,6 +51,8 @@ const mapDispatchToProps = {
dispatchSetInteractiveImportSort: setInteractiveImportSort,
dispatchSetInteractiveImportMode: setInteractiveImportMode,
dispatchClearInteractiveImport: clearInteractiveImport,
+ dispatchUpdateEpisodeFiles: updateEpisodeFiles,
+ dispatchDeleteEpisodeFiles: deleteEpisodeFiles,
dispatchExecuteCommand: executeCommand
};
@@ -44,16 +74,34 @@ class InteractiveImportModalContentConnector extends Component {
const {
downloadId,
seriesId,
- folder
+ seasonNumber,
+ folder,
+ initialSortKey,
+ initialSortDirection,
+ dispatchSetInteractiveImportSort,
+ dispatchFetchInteractiveImportItems
} = this.props;
const {
filterExistingFiles
} = this.state;
- this.props.dispatchFetchInteractiveImportItems({
+ if (initialSortKey) {
+ const sortProps = {
+ sortKey: initialSortKey
+ };
+
+ if (initialSortDirection) {
+ sortProps.sortDirection = initialSortDirection;
+ }
+
+ dispatchSetInteractiveImportSort(sortProps);
+ }
+
+ dispatchFetchInteractiveImportItems({
downloadId,
seriesId,
+ seasonNumber,
folder,
filterExistingFiles
});
@@ -99,10 +147,23 @@ class InteractiveImportModalContentConnector extends Component {
this.props.dispatchSetInteractiveImportMode({ importMode });
}
+ onDeleteSelectedPress = (selected) => {
+ // TODO: Delete selected (if they have episode IDs)
+ }
+
onImportSelectedPress = (selected, importMode) => {
+ const {
+ items,
+ originalItems,
+ dispatchUpdateEpisodeFiles,
+ dispatchExecuteCommand,
+ onModalClose
+ } = this.props;
+
+ const existingFiles = [];
const files = [];
- _.forEach(this.props.items, (item) => {
+ items.forEach((item) => {
const isSelected = selected.indexOf(item.id) > -1;
if (isSelected) {
@@ -112,32 +173,48 @@ class InteractiveImportModalContentConnector extends Component {
episodes,
releaseGroup,
quality,
- language
+ language,
+ episodeFileId
} = item;
if (!series) {
this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' });
- return false;
+ return;
}
if (isNaN(seasonNumber)) {
this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' });
- return false;
+ return;
}
if (!episodes || !episodes.length) {
this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' });
- return false;
+ return;
}
if (!quality) {
this.setState({ interactiveImportErrorMessage: 'Quality must be chosen for each selected file' });
- return false;
+ return;
}
if (!language) {
this.setState({ interactiveImportErrorMessage: 'Language must be chosen for each selected file' });
- return false;
+ return;
+ }
+
+ if (episodeFileId) {
+ const originalItem = originalItems.find((i) => i.id === item.id);
+
+ if (isSameEpisodeFile(item, originalItem)) {
+ existingFiles.push({
+ id: episodeFileId,
+ releaseGroup,
+ quality,
+ language
+ });
+
+ return;
+ }
}
files.push({
@@ -148,22 +225,35 @@ class InteractiveImportModalContentConnector extends Component {
releaseGroup,
quality,
language,
- downloadId: this.props.downloadId
+ downloadId: this.props.downloadId,
+ episodeFileId
});
}
});
- if (!files.length) {
- return;
+ let shouldClose = false;
+
+ if (existingFiles.length) {
+ dispatchUpdateEpisodeFiles({
+ files: existingFiles
+ });
+
+ shouldClose = true;
}
- this.props.dispatchExecuteCommand({
- name: commandNames.INTERACTIVE_IMPORT,
- files,
- importMode
- });
+ if (files.length) {
+ dispatchExecuteCommand({
+ name: commandNames.INTERACTIVE_IMPORT,
+ files,
+ importMode
+ });
+
+ shouldClose = true;
+ }
- this.props.onModalClose();
+ if (shouldClose) {
+ onModalClose();
+ }
}
//
@@ -183,6 +273,7 @@ class InteractiveImportModalContentConnector extends Component {
onSortPress={this.onSortPress}
onFilterExistingFilesChange={this.onFilterExistingFilesChange}
onImportModeChange={this.onImportModeChange}
+ onDeleteSelectedPress={this.onDeleteSelectedPress}
onImportSelectedPress={this.onImportSelectedPress}
/>
);
@@ -192,13 +283,19 @@ class InteractiveImportModalContentConnector extends Component {
InteractiveImportModalContentConnector.propTypes = {
downloadId: PropTypes.string,
seriesId: PropTypes.number,
+ seasonNumber: PropTypes.number,
folder: PropTypes.string,
filterExistingFiles: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
+ initialSortKey: PropTypes.string,
+ initialSortDirection: PropTypes.oneOf(sortDirections.all),
+ originalItems: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchInteractiveImportItems: PropTypes.func.isRequired,
dispatchSetInteractiveImportSort: PropTypes.func.isRequired,
dispatchSetInteractiveImportMode: PropTypes.func.isRequired,
dispatchClearInteractiveImport: PropTypes.func.isRequired,
+ dispatchUpdateEpisodeFiles: PropTypes.func.isRequired,
+ dispatchDeleteEpisodeFiles: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
index 7b3b1c0b4..3512fa827 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
@@ -41,23 +41,35 @@ class InteractiveImportRow extends Component {
componentDidMount() {
const {
+ allowSeriesChange,
id,
series,
seasonNumber,
episodes,
quality,
- language
+ language,
+ episodeFileId,
+ columns
} = this.props;
if (
+ allowSeriesChange &&
series &&
seasonNumber != null &&
episodes.length &&
quality &&
language
) {
- this.props.onSelectedChange({ id, value: true });
+ this.props.onSelectedChange({
+ id,
+ hasEpisodeFileId: !!episodeFileId,
+ value: true
+ });
}
+
+ this.setState({
+ isSeriesColumnVisible: columns.find((c) => c.name === 'series').isVisible
+ });
}
componentDidUpdate(prevProps) {
@@ -104,17 +116,34 @@ class InteractiveImportRow extends Component {
selectRowAfterChange = (value) => {
const {
id,
+ episodeFileId,
isSelected
} = this.props;
if (!isSelected && value === true) {
- this.props.onSelectedChange({ id, value });
+ this.props.onSelectedChange({
+ id,
+ hasEpisodeFileId: !!episodeFileId,
+ value
+ });
}
}
//
// Listeners
+ onSelectedChange = (result) => {
+ const {
+ episodeFileId,
+ onSelectedChange
+ } = this.props;
+
+ onSelectedChange({
+ ...result,
+ hasEpisodeFileId: !!episodeFileId
+ });
+ }
+
onSelectSeriesPress = () => {
this.setState({ isSelectSeriesModalOpen: true });
}
@@ -186,8 +215,7 @@ class InteractiveImportRow extends Component {
size,
rejections,
isReprocessing,
- isSelected,
- onSelectedChange
+ isSelected
} = this.props;
const {
@@ -224,7 +252,7 @@ class InteractiveImportRow extends Component {
-
- {
- showSeriesPlaceholder ? : seriesTitle
- }
-
+ {
+ this.state.isSeriesColumnVisible ?
+
+ {
+ showSeriesPlaceholder ? : seriesTitle
+ }
+ :
+ null
+ }
{
- this.setState({ isInteractiveImportModalOpen: true });
- }
-
- onInteractiveImportModalClose = () => {
- this.setState({ isInteractiveImportModalOpen: false });
- }
-
onEditSeriesPress = () => {
this.setState({ isEditSeriesModalOpen: true });
}
@@ -227,7 +217,6 @@ class SeriesDetails extends Component {
isEditSeriesModalOpen,
isDeleteSeriesModalOpen,
isSeriesHistoryModalOpen,
- isInteractiveImportModalOpen,
isMonitorOptionsModalOpen,
allExpanded,
allCollapsed,
@@ -299,12 +288,6 @@ class SeriesDetails extends Component {
onPress={this.onSeriesHistoryPress}
/>
-
-
-
@@ -677,16 +669,6 @@ class SeriesDetails extends Component {
onModalClose={this.onDeleteSeriesModalClose}
/>
-
-
- {
- seasonNumber === 0 ?
-
- Specials
- :
-
- Season {seasonNumber}
-
- }
+
+ {title}
+
-
@@ -513,6 +519,7 @@ class SeriesDetailsSeason extends Component {
SeriesDetailsSeason.propTypes = {
seriesId: PropTypes.number.isRequired,
+ path: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
seasonNumber: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
diff --git a/frontend/src/Series/Details/SeriesDetailsSeasonConnector.js b/frontend/src/Series/Details/SeriesDetailsSeasonConnector.js
index c69c9810c..2c34d0b49 100644
--- a/frontend/src/Series/Details/SeriesDetailsSeasonConnector.js
+++ b/frontend/src/Series/Details/SeriesDetailsSeasonConnector.js
@@ -34,6 +34,7 @@ function createMapStateToProps() {
columns: episodes.columns,
isSearching,
seriesMonitored: series.monitored,
+ path: series.path,
isSmallScreen: dimensions.isSmallScreen
};
}
diff --git a/frontend/src/Store/Actions/episodeFileActions.js b/frontend/src/Store/Actions/episodeFileActions.js
index e4a0cc951..46da28c87 100644
--- a/frontend/src/Store/Actions/episodeFileActions.js
+++ b/frontend/src/Store/Actions/episodeFileActions.js
@@ -140,28 +140,14 @@ export const actionHandlers = handleThunks({
},
[UPDATE_EPISODE_FILES]: function(getState, payload, dispatch) {
- const {
- episodeFileIds,
- language,
- quality
- } = payload;
+ const { files } = payload;
dispatch(set({ section, isSaving: true }));
- const requestData = {
- episodeFileIds
- };
-
- if (language) {
- requestData.language = language;
- }
-
- if (quality) {
- requestData.quality = quality;
- }
+ const requestData = files;
const promise = createAjaxRequest({
- url: '/episodeFile/editor',
+ url: '/episodeFile/bulk',
method: 'PUT',
dataType: 'json',
data: JSON.stringify(requestData)
@@ -169,23 +155,22 @@ export const actionHandlers = handleThunks({
promise.done((data) => {
dispatch(batchActions([
- ...episodeFileIds.map((id) => {
+ ...files.map((file) => {
+ const id = file.id;
const props = {};
-
- const episodeFile = data.find((file) => file.id === id);
+ const episodeFile = data.find((f) => f.id === id);
props.qualityCutoffNotMet = episodeFile.qualityCutoffNotMet;
props.languageCutoffNotMet = episodeFile.languageCutoffNotMet;
+ props.language = file.language;
+ props.quality = file.quality;
+ props.releaseGroup = file.releaseGroup;
- if (language) {
- props.language = language;
- }
-
- if (quality) {
- props.quality = quality;
- }
-
- return updateItem({ section, id, ...props });
+ return updateItem({
+ section,
+ id,
+ ...props
+ });
}),
set({
diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js
index ee6b3f058..cc55d4bcb 100644
--- a/frontend/src/Store/Actions/interactiveImportActions.js
+++ b/frontend/src/Store/Actions/interactiveImportActions.js
@@ -29,6 +29,7 @@ export const defaultState = {
isPopulated: false,
error: null,
items: [],
+ originalItems: [],
sortKey: 'quality',
sortDirection: sortDirections.DESCENDING,
recentFolders: [],
@@ -127,7 +128,8 @@ export const actionHandlers = handleThunks({
section,
isFetching: false,
isPopulated: true,
- error: null
+ error: null,
+ originalItems: data
})
]));
});
diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs
index 557276771..8f155568b 100644
--- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs
+++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs
@@ -12,6 +12,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
public string FolderName { get; set; }
public int SeriesId { get; set; }
public List EpisodeIds { get; set; }
+ public int? EpisodeFileId { get; set; }
public QualityModel Quality { get; set; }
public Language Language { get; set; }
public string ReleaseGroup { get; set; }
diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs
index 70926e8d2..76f84724f 100644
--- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs
+++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs
@@ -16,6 +16,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
public Series Series { get; set; }
public int? SeasonNumber { get; set; }
public List Episodes { get; set; }
+ public int? EpisodeFileId { get; set; }
public QualityModel Quality { get; set; }
public Language Language { get; set; }
public string ReleaseGroup { get; set; }
diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs
index 000ebdc38..3a4c0bfde 100644
--- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs
+++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs
@@ -22,6 +22,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
{
public interface IManualImportService
{
+ List GetMediaFiles(int seriesId, int? seasonNumber);
List GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles);
ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List episodeIds, string releaseGroup, QualityModel quality, Language language);
}
@@ -38,6 +39,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
private readonly IAggregationService _aggregationService;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
+ private readonly IMediaFileService _mediaFileService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
@@ -51,6 +53,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
IImportApprovedEpisodes importApprovedEpisodes,
ITrackedDownloadService trackedDownloadService,
IDownloadedEpisodesImportService downloadedEpisodesImportService,
+ IMediaFileService mediaFileService,
IEventAggregator eventAggregator,
Logger logger)
{
@@ -64,10 +67,46 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
_importApprovedEpisodes = importApprovedEpisodes;
_trackedDownloadService = trackedDownloadService;
_downloadedEpisodesImportService = downloadedEpisodesImportService;
+ _mediaFileService = mediaFileService;
_eventAggregator = eventAggregator;
_logger = logger;
}
+ public List GetMediaFiles(int seriesId, int? seasonNumber)
+ {
+ var series = _seriesService.GetSeries(seriesId);
+ var directoryInfo = new DirectoryInfo(series.Path);
+ var seriesFiles = seasonNumber.HasValue ? _mediaFileService.GetFilesBySeason(seriesId, seasonNumber.Value) : _mediaFileService.GetFilesBySeries(seriesId);
+
+ var items = seriesFiles.Select(episodeFile => MapItem(episodeFile, series, directoryInfo.Name)).ToList();
+
+ if (!seasonNumber.HasValue)
+ {
+ var mediaFiles = _diskScanService.FilterPaths(series.Path, _diskScanService.GetVideoFiles(series.Path)).ToList();
+ var unmappedFiles = MediaFileService.FilterExistingFiles(mediaFiles, seriesFiles, series);
+
+ items.AddRange(unmappedFiles.Select(file =>
+ new ManualImportItem
+ {
+ Path = Path.Combine(series.Path, file),
+ FolderName = directoryInfo.Name,
+ RelativePath = series.Path.GetRelativePath(file),
+ Name = Path.GetFileNameWithoutExtension(file),
+ Series = series,
+ SeasonNumber = null,
+ Episodes = new List(),
+ ReleaseGroup = string.Empty,
+ Quality = new QualityModel(Quality.Unknown),
+ Language = Language.Unknown,
+ Size = _diskProvider.GetFileSize(file),
+ Rejections = Enumerable.Empty()
+ }
+ ));
+ }
+
+ return items;
+ }
+
public List GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles)
{
if (downloadId.IsNotNullOrWhiteSpace())
@@ -363,6 +402,27 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
return item;
}
+ private ManualImportItem MapItem(EpisodeFile episodeFile, Series series, string folderName)
+ {
+ var item = new ManualImportItem();
+
+ item.Path = Path.Combine(series.Path, episodeFile.RelativePath);
+ item.FolderName = folderName;
+ item.RelativePath = episodeFile.RelativePath;
+ item.Name = Path.GetFileNameWithoutExtension(episodeFile.Path);
+ item.Series = series;
+ item.SeasonNumber = episodeFile.SeasonNumber;
+ item.Episodes = episodeFile.Episodes.Value;
+ item.ReleaseGroup = episodeFile.ReleaseGroup;
+ item.Quality = episodeFile.Quality;
+ item.Language = episodeFile.Language;
+ item.Size = _diskProvider.GetFileSize(item.Path);
+ item.Rejections = Enumerable.Empty();
+ item.EpisodeFileId = episodeFile.Id;
+
+ return item;
+ }
+
public void Execute(ManualImportCommand message)
{
_logger.ProgressTrace("Manually importing {0} files using mode {1}", message.Files.Count, message.ImportMode);
@@ -379,6 +439,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
var episodes = _episodeService.GetEpisodes(file.EpisodeIds);
var fileEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo();
var existingFile = series.Path.IsParentPath(file.Path);
+
TrackedDownload trackedDownload = null;
var localEpisode = new LocalEpisode
@@ -437,7 +498,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
}
}
- _logger.ProgressTrace("Manually imported {0} files", imported.Count);
+ if (imported.Any())
+ {
+ _logger.ProgressTrace("Manually imported {0} files", imported.Count);
+ }
foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList())
{
diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs
index 93fa6aa19..179085138 100644
--- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs
+++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs
@@ -88,11 +88,9 @@ namespace NzbDrone.Core.MediaFiles
public List FilterExistingFiles(List files, Series series)
{
- var seriesFiles = GetFilesBySeries(series.Id).Select(f => Path.Combine(series.Path, f.RelativePath)).ToList();
+ var seriesFiles = GetFilesBySeries(series.Id);
- if (!seriesFiles.Any()) return files;
-
- return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList();
+ return FilterExistingFiles(files, seriesFiles, series);
}
public EpisodeFile Get(int id)
@@ -115,5 +113,15 @@ namespace NzbDrone.Core.MediaFiles
var files = GetFilesBySeries(message.Series.Id);
_mediaFileRepository.DeleteMany(files);
}
+
+ public static List FilterExistingFiles(List files, List seriesFiles, Series series)
+ {
+ var seriesFilePaths = seriesFiles.Select(f => Path.Combine(series.Path, f.RelativePath)).ToList();
+
+ if (!seriesFilePaths.Any()) return files;
+
+ return files.Except(seriesFilePaths, PathEqualityComparer.Instance).ToList();
+ }
+
}
}
diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs
index 6fb6c4837..6feb5102b 100644
--- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs
+++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using Nancy;
using NzbDrone.Core.Datastore.Events;
-using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles;
@@ -44,7 +43,8 @@ namespace Sonarr.Api.V3.EpisodeFiles
UpdateResource = SetQuality;
DeleteResource = DeleteEpisodeFile;
- Put("/editor", episodeFiles => SetQuality());
+ Put("/editor", episodeFiles => SetPropertiesEditor());
+ Put("/bulk", episodeFiles => SetPropertiesBulk());
Delete("/bulk", episodeFiles => DeleteEpisodeFiles());
}
@@ -109,7 +109,8 @@ namespace Sonarr.Api.V3.EpisodeFiles
_mediaFileService.Update(episodeFile);
}
- private object SetQuality()
+ // Deprecated: Use SetPropertiesBulk instead
+ private object SetPropertiesEditor()
{
var resource = Request.Body.FromJson();
var episodeFiles = _mediaFileService.GetFiles(resource.EpisodeFileIds);
@@ -141,8 +142,44 @@ namespace Sonarr.Api.V3.EpisodeFiles
var series = _seriesService.GetSeries(episodeFiles.First().SeriesId);
- return ResponseWithCode(episodeFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification))
- , HttpStatusCode.Accepted);
+ return ResponseWithCode(episodeFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification)), HttpStatusCode.Accepted);
+ }
+
+ private object SetPropertiesBulk()
+ {
+ var resource = Request.Body.FromJson>();
+ var episodeFiles = _mediaFileService.GetFiles(resource.Select(r => r.Id));
+
+ foreach (var episodeFile in episodeFiles)
+ {
+ var resourceEpisodeFile = resource.Single(r => r.Id == episodeFile.Id);
+
+ if (resourceEpisodeFile.Language != null)
+ {
+ episodeFile.Language = resourceEpisodeFile.Language;
+ }
+
+ if (resourceEpisodeFile.Quality != null)
+ {
+ episodeFile.Quality = resourceEpisodeFile.Quality;
+ }
+
+ if (resourceEpisodeFile.SceneName != null && SceneChecker.IsSceneTitle(resourceEpisodeFile.SceneName))
+ {
+ episodeFile.SceneName = resourceEpisodeFile.SceneName;
+ }
+
+ if (resourceEpisodeFile.ReleaseGroup != null)
+ {
+ episodeFile.ReleaseGroup = resourceEpisodeFile.ReleaseGroup;
+ }
+ }
+
+ _mediaFileService.Update(episodeFiles);
+
+ var series = _seriesService.GetSeries(episodeFiles.First().SeriesId);
+
+ return ResponseWithCode(episodeFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification)), HttpStatusCode.Accepted);
}
private void DeleteEpisodeFile(int id)
diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs
index ebcb1a30c..0c754be1c 100644
--- a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs
+++ b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
-using Nancy;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles.EpisodeImport.Manual;
@@ -30,6 +29,12 @@ namespace Sonarr.Api.V3.ManualImport
var downloadId = (string)Request.Query.downloadId;
var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true);
var seriesId = Request.GetNullableIntegerQueryParameter("seriesId", null);
+ var seasonNumber = Request.GetNullableIntegerQueryParameter("seasonNumber", null);
+
+ if (seriesId.HasValue)
+ {
+ return _manualImportService.GetMediaFiles(seriesId.Value, seasonNumber).ToResource().Select(AddQualityWeight).ToList();
+ }
return _manualImportService.GetMediaFiles(folder, downloadId, seriesId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList();
}
diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs
index ea6318545..d11c0e0a4 100644
--- a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs
+++ b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs
@@ -21,6 +21,7 @@ namespace Sonarr.Api.V3.ManualImport
public SeriesResource Series { get; set; }
public int? SeasonNumber { get; set; }
public List Episodes { get; set; }
+ public int? EpisodeFileId { get; set; }
public string ReleaseGroup { get; set; }
public QualityModel Quality { get; set; }
public Language Language { get; set; }
@@ -46,6 +47,7 @@ namespace Sonarr.Api.V3.ManualImport
Series = model.Series.ToResource(),
SeasonNumber = model.SeasonNumber,
Episodes = model.Episodes.ToResource(),
+ EpisodeFileId = model.EpisodeFileId,
ReleaseGroup = model.ReleaseGroup,
Quality = model.Quality,
Language = model.Language,