From 84a758064c112b51c6a2944782da82b7e1555a7c Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 14 Jan 2023 11:45:54 -0600 Subject: [PATCH] Fixed: Manual Import Reprocessing --- .../Album/SelectAlbumModalContentConnector.js | 2 +- ...SelectAlbumReleaseModalContentConnector.js | 2 +- .../SelectArtistModalContentConnector.js | 2 +- .../InteractiveImportModalContent.js | 18 ++- .../InteractiveImportModalContentConnector.js | 5 + .../Interactive/InteractiveImportRow.css | 3 +- .../Interactive/InteractiveImportRow.js | 42 ++++++- .../InteractiveImportModal.js | 7 ++ .../SelectQualityModalContentConnector.js | 18 ++- .../ReleaseGroup/SelectReleaseGroupModal.js | 37 +++++++ .../SelectReleaseGroupModalContent.css | 7 ++ .../SelectReleaseGroupModalContent.js | 103 ++++++++++++++++++ ...SelectReleaseGroupModalContentConnector.js | 54 +++++++++ .../Store/Actions/interactiveImportActions.js | 85 ++++++++++++++- .../ManualImport/ManualImportController.cs | 14 +-- .../ManualImport/ManualImportResource.cs | 2 + .../ManualImportUpdateResource.cs | 29 +++++ src/NzbDrone.Core/Localization/Core/en.json | 7 +- .../TrackImport/Manual/ManualImportItem.cs | 1 + .../TrackImport/Manual/ManualImportService.cs | 12 ++ 20 files changed, 424 insertions(+), 26 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/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js index 27d213d59..12cd88e53 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js @@ -70,7 +70,7 @@ class SelectAlbumModalContentConnector extends Component { }); }); - this.props.saveInteractiveImportItem({ id: ids }); + this.props.saveInteractiveImportItem({ ids }); this.props.onModalClose(true); }; diff --git a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContentConnector.js b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContentConnector.js index ad211684e..427592afb 100644 --- a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContentConnector.js +++ b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContentConnector.js @@ -37,7 +37,7 @@ class SelectAlbumReleaseModalContentConnector extends Component { }); }); - this.props.saveInteractiveImportItem({ id: ids }); + this.props.saveInteractiveImportItem({ ids }); this.props.onModalClose(true); }; diff --git a/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js b/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js index 77fa0fda8..c2cca6c27 100644 --- a/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js +++ b/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js @@ -54,7 +54,7 @@ class SelectArtistModalContentConnector 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 be555d969..f9ee3c59e 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -21,6 +21,7 @@ import SelectAlbumReleaseModal from 'InteractiveImport/AlbumRelease/SelectAlbumR import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal'; 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'; @@ -52,6 +53,11 @@ const columns = [ label: translate('Tracks'), isVisible: true }, + { + name: 'releaseGroup', + label: translate('ReleaseGroup'), + isVisible: true + }, { name: 'quality', label: translate('Quality'), @@ -81,6 +87,7 @@ const filterExistingFilesOptions = { }; const importModeOptions = [ + { key: 'chooseImportMode', value: translate('ChooseImportMethod'), disabled: true }, { key: 'move', value: translate('MoveFiles') }, { key: 'copy', value: translate('HardlinkCopyFiles') } ]; @@ -89,6 +96,7 @@ const SELECT = 'select'; const ARTIST = 'artist'; const ALBUM = 'album'; const ALBUM_RELEASE = 'albumRelease'; +const RELEASE_GROUP = 'releaseGroup'; const QUALITY = 'quality'; const replaceExistingFilesOptions = { @@ -292,7 +300,8 @@ class InteractiveImportModalContent extends Component { { key: SELECT, value: translate('Select...'), disabled: true }, { key: ALBUM, value: translate('SelectAlbum') }, { key: ALBUM_RELEASE, value: translate('SelectAlbumRelease') }, - { key: QUALITY, value: translate('SelectQuality') } + { key: QUALITY, value: translate('SelectQuality') }, + { key: RELEASE_GROUP, value: translate('SelectReleaseGroup') } ]; if (allowArtistChange) { @@ -513,6 +522,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 d5286412c..599225f02 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css @@ -16,10 +16,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 c994a3343..49d3fd0da 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -13,6 +13,7 @@ import { icons, kinds, sortDirections, tooltipPositions } from 'Helpers/Props'; import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal'; import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; +import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal'; import formatBytes from 'Utilities/Number/formatBytes'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; @@ -32,6 +33,7 @@ class InteractiveImportRow extends Component { isSelectArtistModalOpen: false, isSelectAlbumModalOpen: false, isSelectTrackModalOpen: false, + isSelectReleaseGroupModalOpen: false, isSelectQualityModalOpen: false }; } @@ -119,6 +121,10 @@ class InteractiveImportRow extends Component { this.setState({ isSelectTrackModalOpen: true }); }; + onSelectReleaseGroupPress = () => { + this.setState({ isSelectReleaseGroupModalOpen: true }); + }; + onSelectQualityPress = () => { this.setState({ isSelectQualityModalOpen: true }); }; @@ -138,6 +144,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); @@ -156,12 +167,13 @@ class InteractiveImportRow extends Component { albumReleaseId, tracks, quality, + releaseGroup, size, rejections, + isReprocessing, audioTags, additionalFile, isSelected, - isSaving, onSelectedChange } = this.props; @@ -169,6 +181,7 @@ class InteractiveImportRow extends Component { isSelectArtistModalOpen, isSelectAlbumModalOpen, isSelectTrackModalOpen, + isSelectReleaseGroupModalOpen, isSelectQualityModalOpen } = this.state; @@ -185,8 +198,9 @@ class InteractiveImportRow extends Component { const showArtistPlaceholder = isSelected && !artist; const showAlbumNumberPlaceholder = isSelected && !!artist && !album; - const showTrackNumbersPlaceholder = !isSaving && isSelected && !!album && !tracks.length; - const showTrackNumbersLoading = isSaving && !tracks.length; + const showTrackNumbersPlaceholder = !isReprocessing && isSelected && !!album && !tracks.length; + const showTrackNumbersLoading = isReprocessing && !tracks.length; + const showReleaseGroupPlaceholder = isSelected && !releaseGroup; const showQualityPlaceholder = isSelected && !quality; const pathCellContents = ( @@ -253,6 +267,17 @@ class InteractiveImportRow extends Component { } + + { + showReleaseGroupPlaceholder ? + : + releaseGroup + } + + } position={tooltipPositions.LEFT} + canFlip={false} /> : null } @@ -333,6 +359,13 @@ class InteractiveImportRow extends Component { onModalClose={this.onSelectTrackModalClose} /> + + @@ -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 663b98abc..86cf06c5e 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 albumsSection = `${section}.albums`; const trackFilesSection = `${section}.trackFiles`; +let abortCurrentRequest = null; +let currentIds = []; const MAXIMUM_RECENT_FOLDERS = 10; @@ -34,7 +35,7 @@ export const defaultState = { sortKey: 'path', sortDirection: sortDirections.ASCENDING, recentFolders: [], - importMode: 'move', + importMode: 'chooseImportMode', sortPredicates: { path: function(item, direction) { const path = item.path; @@ -156,7 +157,83 @@ 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, + artistId: item.artist ? item.artist.id : undefined, + albumId: item.album ? item.album.id : undefined, + albumReleaseId: item.albumReleaseId ? item.albumReleaseId : undefined, + trackIds: (item.tracks || []).map((e) => e.id), + 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_ALBUMS]: createFetchHandler(albumsSection, '/album'), diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs index eab2f1b83..b11c36a91 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs @@ -32,8 +32,8 @@ namespace Lidarr.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 Lidarr.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 Lidarr.Api.V1.ManualImport Id = resource.Id, Path = resource.Path, Name = resource.Name, - Size = resource.Size, - Artist = resource.Artist == null ? null : _artistService.GetArtist(resource.Artist.Id), - Album = resource.Album == null ? null : _albumService.GetAlbum(resource.Album.Id), - Release = resource.AlbumReleaseId == 0 ? null : _releaseService.GetRelease(resource.AlbumReleaseId), + Artist = resource.ArtistId.HasValue ? _artistService.GetArtist(resource.ArtistId.Value) : null, + Album = resource.AlbumId.HasValue ? _albumService.GetAlbum(resource.AlbumId.Value) : null, + Release = resource.AlbumReleaseId.HasValue ? _releaseService.GetRelease(resource.AlbumReleaseId.Value) : null, Quality = resource.Quality, + ReleaseGroup = resource.ReleaseGroup, DownloadId = resource.DownloadId, AdditionalFile = resource.AdditionalFile, ReplaceExistingFiles = resource.ReplaceExistingFiles, diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs index 960e59262..4b38b4f7c 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs @@ -21,6 +21,7 @@ namespace Lidarr.Api.V1.ManualImport public int AlbumReleaseId { get; set; } public List Tracks { 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; } @@ -50,6 +51,7 @@ namespace Lidarr.Api.V1.ManualImport AlbumReleaseId = model.Release?.Id ?? 0, Tracks = model.Tracks.ToResource(), Quality = model.Quality, + ReleaseGroup = model.ReleaseGroup, // QualityWeight DownloadId = model.DownloadId, diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs new file mode 100644 index 000000000..c544b3500 --- /dev/null +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Lidarr.Api.V1.Albums; +using Lidarr.Api.V1.Tracks; +using Lidarr.Http.REST; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; + +namespace Lidarr.Api.V1.ManualImport +{ + public class ManualImportUpdateResource : RestResource + { + public string Path { get; set; } + public string Name { get; set; } + public int? ArtistId { get; set; } + public int? AlbumId { get; set; } + public int? AlbumReleaseId { get; set; } + public List Tracks { get; set; } + public List TrackIds { 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; } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a6e3e6577..b03d3b4da 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -119,11 +119,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 Lidarr 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 Lidarr is the owner of the file. It's better to ensure the download client uses the same group as Lidarr.", "Clear": "Clear", "ClickToChangeQuality": "Click to change quality", + "ClickToChangeReleaseGroup": "Click to change release group", "ClientPriority": "Client Priority", "CloneIndexer": "Clone Indexer", "CloneProfile": "Clone Profile", @@ -497,11 +499,11 @@ "NotificationTriggers": "Notification Triggers", "NoUpdatesAreAvailable": "No updates are available", "Ok": "Ok", - "OnAlbumDeleteHelpText": "On Album Delete", "OnAlbumDelete": "On Album Delete", - "OnArtistDelete": "On Artist Delete", + "OnAlbumDeleteHelpText": "On Album Delete", "OnApplicationUpdate": "On Application Update", "OnApplicationUpdateHelpText": "On Application Update", + "OnArtistDelete": "On Artist Delete", "OnArtistDeleteHelpText": "On Artist Delete", "OnDownloadFailure": "On Download Failure", "OnDownloadFailureHelpText": "On Download Failure", @@ -692,6 +694,7 @@ "SelectedCountArtistsSelectedInterp": "{0} Artist(s) Selected", "SelectFolder": "Select Folder", "SelectQuality": "Select Quality", + "SelectReleaseGroup": "Select Release Group", "SelectTracks": "Select Tracks", "SendAnonymousUsageData": "Send Anonymous Usage Data", "SetPermissions": "Set Permissions", diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs index f0878b774..27cd5ef04 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public AlbumRelease Release { get; set; } public List Tracks { 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/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 89792ef3c..2d0eee36d 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -17,6 +17,7 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; using NzbDrone.Core.RootFolders; namespace NzbDrone.Core.MediaFiles.TrackImport.Manual @@ -237,7 +238,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual item.Tracks = decision.Item.Tracks; } + 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); }