From 3825ecd3932786a3d63c3bd4476a984b15e69841 Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 14 Jan 2023 11:45:54 -0600 Subject: [PATCH] Fixed: Manual Import Reprocessing --- .../SelectAuthorModalContentConnector.js | 2 +- .../Book/SelectBookModalContentConnector.js | 2 +- .../SelectEditionModalContentConnector.js | 2 +- .../InteractiveImportModalContent.js | 23 +++- .../InteractiveImportModalContentConnector.js | 5 + .../Interactive/InteractiveImportRow.css | 3 +- .../Interactive/InteractiveImportRow.js | 52 ++++++--- .../InteractiveImportModal.js | 7 ++ .../SelectQualityModalContentConnector.js | 18 ++- .../ReleaseGroup/SelectReleaseGroupModal.js | 37 +++++++ .../SelectReleaseGroupModalContent.css | 7 ++ .../SelectReleaseGroupModalContent.js | 103 ++++++++++++++++++ ...SelectReleaseGroupModalContentConnector.js | 54 +++++++++ .../Store/Actions/interactiveImportActions.js | 84 +++++++++++++- src/NzbDrone.Core/Localization/Core/en.json | 8 +- .../BookImport/Manual/ManualImportItem.cs | 1 + .../BookImport/Manual/ManualImportService.cs | 12 ++ .../ManualImport/ManualImportController.cs | 12 +- .../ManualImport/ManualImportResource.cs | 2 + .../ManualImportUpdateResource.cs | 24 ++++ 20 files changed, 422 insertions(+), 36 deletions(-) create mode 100644 frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModal.js create mode 100644 frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.css create mode 100644 frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.js create mode 100644 frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContentConnector.js create mode 100644 src/Readarr.Api.V1/ManualImport/ManualImportUpdateResource.cs diff --git a/frontend/src/InteractiveImport/Author/SelectAuthorModalContentConnector.js b/frontend/src/InteractiveImport/Author/SelectAuthorModalContentConnector.js index c9e3c7dcc..2aeaccc79 100644 --- a/frontend/src/InteractiveImport/Author/SelectAuthorModalContentConnector.js +++ b/frontend/src/InteractiveImport/Author/SelectAuthorModalContentConnector.js @@ -53,7 +53,7 @@ class SelectAuthorModalContentConnector extends Component { }); }); - this.props.saveInteractiveImportItem({ id: ids }); + this.props.saveInteractiveImportItem({ ids }); this.props.onModalClose(true); }; diff --git a/frontend/src/InteractiveImport/Book/SelectBookModalContentConnector.js b/frontend/src/InteractiveImport/Book/SelectBookModalContentConnector.js index a666505af..4277e9436 100644 --- a/frontend/src/InteractiveImport/Book/SelectBookModalContentConnector.js +++ b/frontend/src/InteractiveImport/Book/SelectBookModalContentConnector.js @@ -69,7 +69,7 @@ class SelectBookModalContentConnector extends Component { }); }); - this.props.saveInteractiveImportItem({ id: ids }); + this.props.saveInteractiveImportItem({ ids }); this.props.onModalClose(true); }; diff --git a/frontend/src/InteractiveImport/Edition/SelectEditionModalContentConnector.js b/frontend/src/InteractiveImport/Edition/SelectEditionModalContentConnector.js index e5d0e9340..1b348d41c 100644 --- a/frontend/src/InteractiveImport/Edition/SelectEditionModalContentConnector.js +++ b/frontend/src/InteractiveImport/Edition/SelectEditionModalContentConnector.js @@ -78,7 +78,7 @@ class SelectEditionModalContentConnector extends Component { }); }); - this.props.saveInteractiveImportItem({ id: ids }); + this.props.saveInteractiveImportItem({ ids }); this.props.onModalClose(true); }; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index 55fbf2b1d..43f77705d 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -21,7 +21,9 @@ import SelectBookModal from 'InteractiveImport/Book/SelectBookModal'; import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal'; import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; +import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; import getSelectedIds from 'Utilities/Table/getSelectedIds'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; @@ -46,6 +48,11 @@ const columns = [ label: 'Book', isVisible: true }, + { + name: 'releaseGroup', + label: 'Release Group', + isVisible: true + }, { name: 'quality', label: 'Quality', @@ -75,14 +82,16 @@ const filterExistingFilesOptions = { }; const importModeOptions = [ - { key: 'move', value: 'Move Files' }, - { key: 'copy', value: 'Hardlink/Copy Files' } + { key: 'chooseImportMode', value: translate('ChooseImportMethod'), disabled: true }, + { key: 'move', value: translate('MoveFiles') }, + { key: 'copy', value: translate('HardlinkCopyFiles') } ]; const SELECT = 'select'; const AUTHOR = 'author'; const BOOK = 'book'; const EDITION = 'edition'; +const RELEASE_GROUP = 'releaseGroup'; const QUALITY = 'quality'; const replaceExistingFilesOptions = { @@ -280,7 +289,8 @@ class InteractiveImportModalContent extends Component { { key: SELECT, value: 'Select...', disabled: true }, { key: BOOK, value: 'Select Book' }, { key: EDITION, value: 'Select Edition' }, - { key: QUALITY, value: 'Select Quality' } + { key: QUALITY, value: 'Select Quality' }, + { key: RELEASE_GROUP, value: 'Select ReleaseGroup' } ]; if (allowAuthorChange) { @@ -483,6 +493,13 @@ class InteractiveImportModalContent extends Component { onModalClose={this.onSelectModalClose} /> + + { const files = []; + if (importMode === 'chooseImportMethod') { + this.setState({ interactiveImportErrorMessage: 'An import mode must be selected' }); + return; + } + _.forEach(this.props.items, (item) => { const isSelected = selected.indexOf(item.id) > -1; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css index 742fb904e..76e7182bc 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css @@ -17,10 +17,11 @@ cursor: pointer; } -.loading { +.reprocessing { composes: loading from '~Components/Loading/LoadingIndicator.css'; margin-top: 0; + text-align: start; } .additionalFile { diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index 3f0bbbcd0..674402609 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import BookQuality from 'Book/BookQuality'; import FileDetails from 'BookFile/FileDetails'; import Icon from 'Components/Icon'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton'; @@ -15,6 +14,7 @@ import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal'; import SelectBookModal from 'InteractiveImport/Book/SelectBookModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; +import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; @@ -32,6 +32,7 @@ class InteractiveImportRow extends Component { isDetailsModalOpen: false, isSelectAuthorModalOpen: false, isSelectBookModalOpen: false, + isSelectReleaseGroupModalOpen: false, isSelectQualityModalOpen: false }; } @@ -123,6 +124,10 @@ class InteractiveImportRow extends Component { this.setState({ isSelectBookModalOpen: true }); }; + onSelectReleaseGroupPress = () => { + this.setState({ isSelectReleaseGroupModalOpen: true }); + }; + onSelectQualityPress = () => { this.setState({ isSelectQualityModalOpen: true }); }; @@ -137,6 +142,11 @@ class InteractiveImportRow extends Component { this.selectRowAfterChange(changed); }; + onSelectReleaseGroupModalClose = (changed) => { + this.setState({ isSelectReleaseGroupModalOpen: false }); + this.selectRowAfterChange(changed); + }; + onSelectQualityModalClose = (changed) => { this.setState({ isSelectQualityModalOpen: false }); this.selectRowAfterChange(changed); @@ -153,19 +163,21 @@ class InteractiveImportRow extends Component { author, book, quality, + releaseGroup, size, rejections, additionalFile, isSelected, + isReprocessing, onSelectedChange, - audioTags, - isSaving + audioTags } = this.props; const { isDetailsModalOpen, isSelectAuthorModalOpen, isSelectBookModalOpen, + isSelectReleaseGroupModalOpen, isSelectQualityModalOpen } = this.state; @@ -176,7 +188,8 @@ class InteractiveImportRow extends Component { } const showAuthorPlaceholder = isSelected && !author; - const showBookNumberPlaceholder = isSelected && !!author && !book; + const showBookNumberPlaceholder = !isReprocessing && isSelected && !!author && !book; + const showReleaseGroupPlaceholder = isSelected && !releaseGroup; const showQualityPlaceholder = isSelected && !quality; const pathCellContents = ( @@ -237,6 +250,17 @@ class InteractiveImportRow extends Component { } + + { + showReleaseGroupPlaceholder ? + : + releaseGroup + } + + { - isSaving && - - } - { - !isSaving && rejections && rejections.length ? + rejections && rejections.length ? } position={tooltipPositions.LEFT} + canFlip={false} /> : null } @@ -322,6 +340,13 @@ class InteractiveImportRow extends Component { onModalClose={this.onSelectBookModalClose} /> + + @@ -73,7 +75,12 @@ InteractiveImportModal.propTypes = { isOpen: PropTypes.bool.isRequired, folder: PropTypes.string, downloadId: PropTypes.string, + modalTitle: PropTypes.string.isRequired, onModalClose: PropTypes.func.isRequired }; +InteractiveImportModal.defaultProps = { + modalTitle: 'Manual Import' +}; + export default InteractiveImportModal; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js b/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js index fca7e58a8..0124969a0 100644 --- a/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; +import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; import getQualities from 'Utilities/Quality/getQualities'; import SelectQualityModalContent from './SelectQualityModalContent'; @@ -31,7 +31,8 @@ function createMapStateToProps() { const mapDispatchToProps = { dispatchFetchQualityProfileSchema: fetchQualityProfileSchema, - dispatchUpdateInteractiveImportItems: updateInteractiveImportItems + dispatchUpdateInteractiveImportItems: updateInteractiveImportItems, + dispatchSaveInteractiveImportItems: saveInteractiveImportItem }; class SelectQualityModalContentConnector extends Component { @@ -49,6 +50,12 @@ class SelectQualityModalContentConnector extends Component { // Listeners onQualitySelect = ({ qualityId, proper, real }) => { + const { + ids, + dispatchUpdateInteractiveImportItems, + dispatchSaveInteractiveImportItems + } = this.props; + const quality = _.find(this.props.items, (item) => item.id === qualityId); @@ -57,14 +64,16 @@ class SelectQualityModalContentConnector extends Component { real: real ? 1 : 0 }; - this.props.dispatchUpdateInteractiveImportItems({ - ids: this.props.ids, + dispatchUpdateInteractiveImportItems({ + ids, quality: { quality, revision } }); + dispatchSaveInteractiveImportItems({ ids }); + this.props.onModalClose(true); }; @@ -89,6 +98,7 @@ SelectQualityModalContentConnector.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired, + dispatchSaveInteractiveImportItems: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModal.js b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModal.js new file mode 100644 index 000000000..04f6e6af3 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectReleaseGroupModalContentConnector from './SelectReleaseGroupModalContentConnector'; + +class SelectReleaseGroupModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectReleaseGroupModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectReleaseGroupModal; diff --git a/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.css b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.css new file mode 100644 index 000000000..72dfb1cb6 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.css @@ -0,0 +1,7 @@ +.modalBody { + composes: modalBody from '~Components/Modal/ModalBody.css'; + + display: flex; + flex: 1 1 auto; + flex-direction: column; +} diff --git a/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.js b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.js new file mode 100644 index 000000000..91a9303e2 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes, kinds, scrollDirections } from 'Helpers/Props'; +import styles from './SelectReleaseGroupModalContent.css'; + +class SelectReleaseGroupModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + releaseGroup + } = props; + + this.state = { + releaseGroup + }; + } + + // + // Listeners + + onReleaseGroupChange = ({ value }) => { + this.setState({ releaseGroup: value }); + }; + + onReleaseGroupSelect = () => { + this.props.onReleaseGroupSelect(this.state); + }; + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + releaseGroup + } = this.state; + + return ( + + + Manual Import - Set Release Group + + + +
+ + Release Group + + + +
+
+ + + + + + +
+ ); + } +} + +SelectReleaseGroupModalContent.propTypes = { + releaseGroup: PropTypes.string.isRequired, + onReleaseGroupSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectReleaseGroupModalContent; diff --git a/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContentConnector.js b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContentConnector.js new file mode 100644 index 000000000..4f05afbb4 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContentConnector.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; +import SelectReleaseGroupModalContent from './SelectReleaseGroupModalContent'; + +const mapDispatchToProps = { + dispatchUpdateInteractiveImportItems: updateInteractiveImportItems, + dispatchSaveInteractiveImportItems: saveInteractiveImportItem +}; + +class SelectReleaseGroupModalContentConnector extends Component { + + // + // Listeners + + onReleaseGroupSelect = ({ releaseGroup }) => { + const { + ids, + dispatchUpdateInteractiveImportItems, + dispatchSaveInteractiveImportItems + } = this.props; + + dispatchUpdateInteractiveImportItems({ + ids, + releaseGroup + }); + + dispatchSaveInteractiveImportItems({ ids }); + + this.props.onModalClose(true); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +SelectReleaseGroupModalContentConnector.propTypes = { + ids: PropTypes.arrayOf(PropTypes.number).isRequired, + dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired, + dispatchSaveInteractiveImportItems: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(null, mapDispatchToProps)(SelectReleaseGroupModalContentConnector); diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index 451f2a560..129d4e167 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -5,10 +5,9 @@ import { sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import updateSectionState from 'Utilities/State/updateSectionState'; -import { set, update } from './baseActions'; +import { set, update, updateItem } from './baseActions'; import createFetchHandler from './Creators/createFetchHandler'; import createHandleActions from './Creators/createHandleActions'; -import createSaveProviderHandler from './Creators/createSaveProviderHandler'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; // @@ -18,6 +17,8 @@ export const section = 'interactiveImport'; const booksSection = `${section}.books`; const bookFilesSection = `${section}.bookFiles`; +let abortCurrentRequest = null; +let currentIds = []; const MAXIMUM_RECENT_FOLDERS = 10; @@ -34,7 +35,7 @@ export const defaultState = { sortKey: 'quality', sortDirection: sortDirections.DESCENDING, recentFolders: [], - importMode: 'move', + importMode: 'chooseImportMode', sortPredicates: { path: function(item, direction) { const path = item.path; @@ -156,7 +157,82 @@ export const actionHandlers = handleThunks({ }); }, - [SAVE_INTERACTIVE_IMPORT_ITEM]: createSaveProviderHandler(section, '/manualimport', {}, true), + [SAVE_INTERACTIVE_IMPORT_ITEM]: function(getState, payload, dispatch) { + if (abortCurrentRequest) { + abortCurrentRequest(); + } + + dispatch(batchActions([ + ...currentIds.map((id) => updateItem({ + section, + id, + isReprocessing: false, + updateOnly: true + })), + ...payload.ids.map((id) => updateItem({ + section, + id, + isReprocessing: true, + updateOnly: true + })) + ])); + + const items = getState()[section].items; + + const requestPayload = payload.ids.map((id) => { + const item = items.find((i) => i.id === id); + + return { + id, + path: item.path, + authorId: item.author ? item.author.id : undefined, + bookId: item.book ? item.book.id : undefined, + foreignEditionId: item.foreignEditionId ? item.ForeignEditionId : undefined, + quality: item.quality, + releaseGroup: item.releaseGroup, + downloadId: item.downloadId, + additionalFile: item.additionalFile, + replaceExistingFiles: item.replaceExistingFiles, + disableReleaseSwitching: item.disableReleaseSwitching + }; + }); + + const { request, abortRequest } = createAjaxRequest({ + method: 'POST', + url: '/manualimport', + contentType: 'application/json', + data: JSON.stringify(requestPayload) + }); + + abortCurrentRequest = abortRequest; + currentIds = payload.ids; + + request.done((data) => { + dispatch(batchActions( + data.map((item) => updateItem({ + section, + ...item, + isReprocessing: false, + updateOnly: true + })) + )); + }); + + request.fail((xhr) => { + if (xhr.aborted) { + return; + } + + dispatch(batchActions( + payload.ids.map((id) => updateItem({ + section, + id, + isReprocessing: false, + updateOnly: true + })) + )); + }); + }, [FETCH_INTERACTIVE_IMPORT_BOOKS]: createFetchHandler(booksSection, '/book'), diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index bd2e82d9e..db05b2a86 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -110,11 +110,13 @@ "ChmodFolder": "chmod Folder", "ChmodFolderHelpText": "Octal, applied during import/rename to media folders and files (without execute bits)", "ChmodFolderHelpTextWarning": "This only works if the user running Readarr is the owner of the file. It's better to ensure the download client sets the permissions properly.", + "ChooseImportMethod": "Choose Import Method", "ChownGroup": "chown Group", "ChownGroupHelpText": "Group name or gid. Use gid for remote file systems.", "ChownGroupHelpTextWarning": "This only works if the user running Readarr is the owner of the file. It's better to ensure the download client uses the same group as Readarr.", "Clear": "Clear", "ClickToChangeQuality": "Click to change quality", + "ClickToChangeReleaseGroup": "Click to change release group", "ClientPriority": "Client Priority", "CloneIndexer": "Clone Indexer", "CloneProfile": "Clone Profile", @@ -298,6 +300,7 @@ "GrabReleaseMessageText": "Readarr was unable to determine which author and book this release was for. Readarr may be unable to automatically import this release. Do you want to grab '{0}'?", "GrabSelected": "Grab Selected", "Group": "Group", + "HardlinkCopyFiles": "Hardlink/Copy Files", "HasMonitoredBooksNoMonitoredBooksForThisAuthor": "No monitored books for this author", "HasPendingChangesNoChanges": "No Changes", "HasPendingChangesSaveChanges": "Save Changes", @@ -457,6 +460,7 @@ "MonoVersion": "Mono Version", "MoreInfo": "More Info", "MountCheckMessage": "Mount containing an author path is mounted read-only: ", + "MoveFiles": "Move Files", "MusicBrainzAuthorID": "MusicBrainz Author ID", "MusicBrainzBookID": "MusicBrainz Book ID", "MusicbrainzId": "Musicbrainz Id", @@ -487,6 +491,8 @@ "NotificationTriggers": "Notification Triggers", "NotMonitored": "Not Monitored", "NoUpdatesAreAvailable": "No updates are available", + "OnApplicationUpdate": "On Application Update", + "OnApplicationUpdateHelpText": "On Application Update", "OnAuthorDelete": "On Author Delete", "OnAuthorDeleteHelpText": "On Author Delete", "OnBookDelete": "On Book Delete", @@ -497,8 +503,6 @@ "OnBookFileDeleteHelpText": "On Book File Delete", "OnBookRetagHelpText": "On Book Retag", "OnBookTagUpdate": "On Book Tag Update", - "OnApplicationUpdate": "On Application Update", - "OnApplicationUpdateHelpText": "On Application Update", "OnDownloadFailure": "On Download Failure", "OnDownloadFailureHelpText": "On Download Failure", "OnGrab": "On Grab", diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportItem.cs index 9184ae2a8..0421ca95c 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportItem.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual public Book Book { get; set; } public Edition Edition { get; set; } public QualityModel Quality { get; set; } + public string ReleaseGroup { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } public ParsedTrackInfo Tags { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs index 71a8f6173..5f7346112 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs @@ -19,6 +19,7 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; using NzbDrone.Core.RootFolders; namespace NzbDrone.Core.MediaFiles.BookImport.Manual @@ -236,7 +237,18 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual item.Edition = decision.Item.Edition; } + if (item.Quality?.Quality == Quality.Unknown) + { + item.Quality = decision.Item.Quality; + } + + if (item.ReleaseGroup.IsNullOrWhiteSpace()) + { + item.ReleaseGroup = decision.Item.ReleaseGroup; + } + item.Rejections = decision.Rejections; + item.Size = decision.Item.Size; result.Add(item); } diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportController.cs b/src/Readarr.Api.V1/ManualImport/ManualImportController.cs index 053beb0c8..d36e5d2ff 100644 --- a/src/Readarr.Api.V1/ManualImport/ManualImportController.cs +++ b/src/Readarr.Api.V1/ManualImport/ManualImportController.cs @@ -32,8 +32,8 @@ namespace Readarr.Api.V1.ManualImport _logger = logger; } - [HttpPut] - public IActionResult UpdateItems(List resource) + [HttpPost] + public IActionResult UpdateItems(List resource) { return Accepted(UpdateImportItems(resource)); } @@ -65,7 +65,7 @@ namespace Readarr.Api.V1.ManualImport return item; } - private List UpdateImportItems(List resources) + private List UpdateImportItems(List resources) { var items = new List(); foreach (var resource in resources) @@ -75,11 +75,11 @@ namespace Readarr.Api.V1.ManualImport Id = resource.Id, Path = resource.Path, Name = resource.Name, - Size = resource.Size, - Author = resource.Author == null ? null : _authorService.GetAuthor(resource.Author.Id), - Book = resource.Book == null ? null : _bookService.GetBook(resource.Book.Id), + Author = resource.AuthorId.HasValue ? _authorService.GetAuthor(resource.AuthorId.Value) : null, + Book = resource.BookId.HasValue ? _bookService.GetBook(resource.BookId.Value) : null, Edition = resource.ForeignEditionId == null ? null : _editionService.GetEditionByForeignEditionId(resource.ForeignEditionId), Quality = resource.Quality, + ReleaseGroup = resource.ReleaseGroup, DownloadId = resource.DownloadId, AdditionalFile = resource.AdditionalFile, ReplaceExistingFiles = resource.ReplaceExistingFiles, diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs index f7341df30..763d6959a 100644 --- a/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs +++ b/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs @@ -19,6 +19,7 @@ namespace Readarr.Api.V1.ManualImport public BookResource Book { get; set; } public string ForeignEditionId { get; set; } public QualityModel Quality { get; set; } + public string ReleaseGroup { get; set; } public int QualityWeight { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } @@ -47,6 +48,7 @@ namespace Readarr.Api.V1.ManualImport Book = model.Book.ToResource(), ForeignEditionId = model.Edition?.ForeignEditionId ?? model.Book?.Editions.Value.Single(x => x.Monitored).ForeignEditionId, Quality = model.Quality, + ReleaseGroup = model.ReleaseGroup, //QualityWeight DownloadId = model.DownloadId, diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportUpdateResource.cs b/src/Readarr.Api.V1/ManualImport/ManualImportUpdateResource.cs new file mode 100644 index 000000000..b87f12489 --- /dev/null +++ b/src/Readarr.Api.V1/ManualImport/ManualImportUpdateResource.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Qualities; +using Readarr.Http.REST; + +namespace Readarr.Api.V1.ManualImport +{ + public class ManualImportUpdateResource : RestResource + { + public string Path { get; set; } + public string Name { get; set; } + public int? AuthorId { get; set; } + public int? BookId { get; set; } + public string ForeignEditionId { get; set; } + public QualityModel Quality { get; set; } + public string ReleaseGroup { get; set; } + public string DownloadId { get; set; } + public bool AdditionalFile { get; set; } + public bool ReplaceExistingFiles { get; set; } + public bool DisableReleaseSwitching { get; set; } + + public IEnumerable Rejections { get; set; } + } +}