diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js index cf48c36d2..3eb438e75 100644 --- a/frontend/src/Activity/History/HistoryRow.js +++ b/frontend/src/Activity/History/HistoryRow.js @@ -58,7 +58,9 @@ class HistoryRow extends Component { album, track, language, + languageCutoffNotMet, quality, + qualityCutoffNotMet, eventType, sourceTitle, date, @@ -135,6 +137,7 @@ class HistoryRow extends Component { ); @@ -145,6 +148,7 @@ class HistoryRow extends Component { ); @@ -233,7 +237,9 @@ HistoryRow.propTypes = { album: PropTypes.object, track: PropTypes.object, language: PropTypes.object.isRequired, + languageCutoffNotMet: PropTypes.bool.isRequired, quality: PropTypes.object.isRequired, + qualityCutoffNotMet: PropTypes.bool.isRequired, eventType: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired, date: PropTypes.string.isRequired, diff --git a/frontend/src/Activity/activity.less b/frontend/src/Activity/activity.less deleted file mode 100644 index c6d9b6d2a..000000000 --- a/frontend/src/Activity/activity.less +++ /dev/null @@ -1,27 +0,0 @@ - -.queue-status-cell .popover { - max-width: 800px; -} - -.queue { - .protocol-cell { - text-align: center; - width: 80px; - } - - .episode-number-cell { - min-width: 90px; - } -} - -.remove-from-queue-modal { - .form-horizontal { - margin-top: 20px; - } -} - -.history-detail-modal { - .info { - word-wrap: break-word; - } -} diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.css b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.css index 1df1b8c90..0a61ca509 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.css +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.css @@ -19,6 +19,12 @@ height: 35px; } +.loadingButton { + composes: importButton; + + margin-left: 10px; +} + .loading { composes: loading from 'Components/Loading/LoadingIndicator.css'; diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.js index 2e4440620..6cae9f6e2 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.js +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.js @@ -2,6 +2,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; import SpinnerButton from 'Components/Link/SpinnerButton'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import CheckInput from 'Components/Form/CheckInput'; @@ -117,7 +118,8 @@ class ImportArtistFooter extends Component { isMetadataProfileIdMixed, showLanguageProfile, showMetadataProfile, - onImportPress + onImportPress, + onCancelLookupPress } = this.props; const { @@ -227,10 +229,21 @@ class ImportArtistFooter extends Component { { isLookingUpArtist && - + + } + + { + isLookingUpArtist && + } { @@ -261,7 +274,8 @@ ImportArtistFooter.propTypes = { showLanguageProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired, onInputChange: PropTypes.func.isRequired, - onImportPress: PropTypes.func.isRequired + onImportPress: PropTypes.func.isRequired, + onCancelLookupPress: PropTypes.func.isRequired }; export default ImportArtistFooter; diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooterConnector.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooterConnector.js index bf3c901ec..ede45dffd 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooterConnector.js +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooterConnector.js @@ -2,6 +2,7 @@ import _ from 'lodash'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import ImportArtistFooter from './ImportArtistFooter'; +import { cancelLookupArtist } from 'Store/Actions/importArtistActions'; function isMixed(items, selectedIds, defaultValue, key) { return _.some(items, (artist) => { @@ -23,11 +24,11 @@ function createMapStateToProps() { albumFolder: defaultAlbumFolder } = addArtist.defaults; - const items = importArtist.items; - - const isLookingUpArtist = _.some(importArtist.items, (artist) => { - return !artist.isPopulated && artist.error == null; - }); + const { + isLookingUpArtist, + isImporting, + items + } = importArtist; const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor'); const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId'); @@ -37,8 +38,8 @@ function createMapStateToProps() { return { selectedCount: selectedIds.length, - isImporting: importArtist.isImporting, isLookingUpArtist, + isImporting, defaultMonitor, defaultQualityProfileId, defaultLanguageProfileId, @@ -54,4 +55,8 @@ function createMapStateToProps() { ); } -export default connect(createMapStateToProps)(ImportArtistFooter); +const mapDispatchToProps = { + onCancelLookupPress: cancelLookupArtist +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistFooter); diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js index f87164a1f..8a7de50b3 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js @@ -10,12 +10,6 @@ class ImportArtistTable extends Component { // // Lifecycle - constructor(props, context) { - super(props, context); - - this._table = null; - } - componentDidMount() { const { unmappedFolders, @@ -101,22 +95,11 @@ class ImportArtistTable extends Component { return; } }); - - // Forces the table to re-render if the selected state - // has changed otherwise it will be stale. - - if (prevProps.selectedState !== selectedState && this._table) { - this._table.forceUpdateGrid(); - } } // // Control - setTableRef = (ref) => { - this._table = ref; - } - rowRenderer = ({ key, rowIndex, style }) => { const { rootFolderId, @@ -156,6 +139,7 @@ class ImportArtistTable extends Component { showLanguageProfile, showMetadataProfile, scrollTop, + selectedState, onSelectAllChange, onScroll } = this.props; @@ -166,7 +150,6 @@ class ImportArtistTable extends Component { return ( } + selectedState={selectedState} onScroll={onScroll} /> ); diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css index 4d9159a70..c5023c00d 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css @@ -66,6 +66,5 @@ .searchInput { composes: text from 'Components/Form/TextInput.css'; - border-top-left-radius: 0; - border-bottom-left-radius: 0; + border-radius: 0; } diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js index e0c883449..533a0bc46 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js @@ -5,6 +5,7 @@ import TetherComponent from 'react-tether'; import { icons, kinds } from 'Helpers/Props'; import Icon from 'Components/Icon'; import SpinnerIcon from 'Components/SpinnerIcon'; +import FormInputButton from 'Components/Form/FormInputButton'; import Link from 'Components/Link/Link'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import TextInput from 'Components/Form/TextInput'; @@ -99,6 +100,10 @@ class ImportArtistSelectArtist extends Component { }); } + onRefreshPress = () => { + this.props.onSearchInputChange(this.state.term); + } + onArtistSelect = (foreignArtistId) => { this.setState({ isOpen: false }); @@ -116,7 +121,8 @@ class ImportArtistSelectArtist extends Component { isPopulated, error, items, - queued + queued, + isLookingUpArtist } = this.props; const errorMessage = error && @@ -137,7 +143,7 @@ class ImportArtistSelectArtist extends Component { onPress={this.onPress} > { - queued && !isPopulated && + isLookingUpArtist && queued && !isPopulated &&
- +
+ + + +
@@ -253,6 +266,7 @@ ImportArtistSelectArtist.propTypes = { error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, queued: PropTypes.bool.isRequired, + isLookingUpArtist: PropTypes.bool.isRequired, onSearchInputChange: PropTypes.func.isRequired, onArtistSelect: PropTypes.func.isRequired }; diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtistConnector.js b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtistConnector.js index 21662faa7..21e2bcab2 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtistConnector.js +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtistConnector.js @@ -9,9 +9,13 @@ import ImportArtistSelectArtist from './ImportArtistSelectArtist'; function createMapStateToProps() { return createSelector( + (state) => state.importArtist.isLookingUpArtist, createImportArtistItemSelector(), - (item) => { - return item; + (isLookingUpArtist, item) => { + return { + isLookingUpArtist, + ...item + }; } ); } @@ -29,7 +33,8 @@ class ImportArtistSelectArtistConnector extends Component { onSearchInputChange = (term) => { this.props.queueLookupArtist({ name: this.props.id, - term + term, + topOfQueue: true }); } diff --git a/frontend/src/Album/EpisodeLanguage.js b/frontend/src/Album/EpisodeLanguage.js index 7a5a24963..52c8b3390 100644 --- a/frontend/src/Album/EpisodeLanguage.js +++ b/frontend/src/Album/EpisodeLanguage.js @@ -1,11 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; import Label from 'Components/Label'; +import { kinds } from 'Helpers/Props'; function EpisodeLanguage(props) { const { className, - language + language, + isCutoffNotMet } = props; if (!language) { @@ -13,7 +15,10 @@ function EpisodeLanguage(props) { } return ( -
} + { + showMonitored && +
+ {monitored ? 'Monitored' : 'Unmonitored'} +
+ } + { showQualityProfile &&
@@ -214,6 +222,7 @@ ArtistIndexBanner.propTypes = { bannerHeight: PropTypes.number.isRequired, detailedProgressBar: PropTypes.bool.isRequired, showTitle: PropTypes.bool.isRequired, + showMonitored: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired, qualityProfile: PropTypes.object.isRequired, showRelativeDates: PropTypes.bool.isRequired, diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js index 986632ab4..12196962a 100644 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js @@ -39,6 +39,7 @@ function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions) const { detailedProgressBar, showTitle, + showMonitored, showQualityProfile } = bannerOptions; @@ -55,6 +56,10 @@ function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions) heights.push(19); } + if (showMonitored) { + heights.push(19); + } + if (showQualityProfile) { heights.push(19); } @@ -213,6 +218,7 @@ class ArtistIndexBanners extends Component { const { detailedProgressBar, showTitle, + showMonitored, showQualityProfile } = bannerOptions; @@ -231,12 +237,16 @@ class ArtistIndexBanners extends Component { bannerHeight={bannerHeight} detailedProgressBar={detailedProgressBar} showTitle={showTitle} + showMonitored={showMonitored} showQualityProfile={showQualityProfile} showRelativeDates={showRelativeDates} shortDateFormat={shortDateFormat} timeFormat={timeFormat} style={style} - {...artist} + artistId={artist.id} + languageProfileId={artist.languageProfileId} + qualityProfileId={artist.qualityProfileId} + metadataProfileId={artist.metadataProfileId} /> ); } diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js index d320acea5..c3d9b321a 100644 --- a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js +++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js @@ -30,6 +30,7 @@ class ArtistIndexBannerOptionsModalContent extends Component { detailedProgressBar: props.detailedProgressBar, size: props.size, showTitle: props.showTitle, + showMonitored: props.showMonitored, showQualityProfile: props.showQualityProfile }; } @@ -39,6 +40,7 @@ class ArtistIndexBannerOptionsModalContent extends Component { detailedProgressBar, size, showTitle, + showMonitored, showQualityProfile } = this.props; @@ -56,6 +58,10 @@ class ArtistIndexBannerOptionsModalContent extends Component { state.showTitle = showTitle; } + if (showMonitored !== prevProps.showMonitored) { + state.showMonitored = showMonitored; + } + if (showQualityProfile !== prevProps.showQualityProfile) { state.showQualityProfile = showQualityProfile; } @@ -68,11 +74,11 @@ class ArtistIndexBannerOptionsModalContent extends Component { // // Listeners - onChangeOption = ({ name, value }) => { + onChangeBannerOption = ({ name, value }) => { this.setState({ [name]: value }, () => { - this.props.onChangeOption({ [name]: value }); + this.props.onChangeBannerOption({ [name]: value }); }); } @@ -88,6 +94,7 @@ class ArtistIndexBannerOptionsModalContent extends Component { detailedProgressBar, size, showTitle, + showMonitored, showQualityProfile } = this.state; @@ -107,7 +114,7 @@ class ArtistIndexBannerOptionsModalContent extends Component { name="size" value={size} values={bannerSizeOptions} - onChange={this.onChangeOption} + onChange={this.onChangeBannerOption} /> @@ -119,7 +126,7 @@ class ArtistIndexBannerOptionsModalContent extends Component { name="detailedProgressBar" value={detailedProgressBar} helpText="Show text on progess bar" - onChange={this.onChangeOption} + onChange={this.onChangeBannerOption} /> @@ -131,7 +138,19 @@ class ArtistIndexBannerOptionsModalContent extends Component { name="showTitle" value={showTitle} helpText="Show artist name under banner" - onChange={this.onChangeOption} + onChange={this.onChangeBannerOption} + /> + + + + Show Monitored + + @@ -143,7 +162,7 @@ class ArtistIndexBannerOptionsModalContent extends Component { name="showQualityProfile" value={showQualityProfile} helpText="Show quality profile under banner" - onChange={this.onChangeOption} + onChange={this.onChangeBannerOption} /> @@ -166,7 +185,8 @@ ArtistIndexBannerOptionsModalContent.propTypes = { showTitle: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired, detailedProgressBar: PropTypes.bool.isRequired, - onChangeOption: PropTypes.func.isRequired, + onChangeBannerOption: PropTypes.func.isRequired, + showMonitored: PropTypes.bool.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js index 0ea742781..884edd05d 100644 --- a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js +++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js @@ -14,7 +14,7 @@ function createMapStateToProps() { function createMapDispatchToProps(dispatch, props) { return { - onChangeOption(payload) { + onChangeBannerOption(payload) { dispatch(setArtistBannerOption(payload)); } }; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js index 059e3c63d..12b9fc04a 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js @@ -5,6 +5,7 @@ import { icons } from 'Helpers/Props'; import dimensions from 'Styles/Variables/dimensions'; import fonts from 'Styles/Variables/fonts'; import IconButton from 'Components/Link/IconButton'; +import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import ArtistPoster from 'Artist/ArtistPoster'; @@ -188,6 +189,7 @@ class ArtistIndexOverview extends Component { @@ -77,7 +80,23 @@ function ArtistIndexOverviewInfo(props) { } { - isVisible('qualityProfileId', showQualityProfile, qualityProfile, sortKey) && maxRows > 1 && + isVisible('monitored', showMonitored, monitored, sortKey) && maxRows > 1 && +
+ + + {monitoredText} +
+ } + + { + isVisible('qualityProfileId', showQualityProfile, qualityProfile, sortKey) && maxRows > 2 &&
2 && + isVisible('added', showAdded, added, sortKey) && maxRows > 3 &&
3 && + isVisible('albumCount', showAlbumCount, albumCount, sortKey) && maxRows > 4 &&
4 && + isVisible('path', showPath, path, sortKey) && maxRows > 5 &&
5 && + isVisible('sizeOnDisk', showSizeOnDisk, sizeOnDisk, sortKey) && maxRows > 6 &&
); } diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js index b5bf02b45..6ae1d8993 100644 --- a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js +++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js @@ -29,6 +29,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component { this.state = { detailedProgressBar: props.detailedProgressBar, size: props.size, + showMonitored: props.showMonitored, showQualityProfile: props.showQualityProfile, showPreviousAiring: props.showPreviousAiring, showAdded: props.showAdded, @@ -42,6 +43,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component { const { detailedProgressBar, size, + showMonitored, showQualityProfile, showPreviousAiring, showAdded, @@ -60,6 +62,10 @@ class ArtistIndexOverviewOptionsModalContent extends Component { state.size = size; } + if (showMonitored !== prevProps.showMonitored) { + state.showMonitored = showMonitored; + } + if (showQualityProfile !== prevProps.showQualityProfile) { state.showQualityProfile = showQualityProfile; } @@ -111,6 +117,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component { const { detailedProgressBar, size, + showMonitored, showQualityProfile, showPreviousAiring, showAdded, @@ -152,6 +159,18 @@ class ArtistIndexOverviewOptionsModalContent extends Component { + Show Monitored + + + + + + Show Quality Profile } + { + showMonitored && +
+ {monitored ? 'Monitored' : 'Unmonitored'} +
+ } + { showQualityProfile &&
@@ -214,6 +222,7 @@ ArtistIndexPoster.propTypes = { posterHeight: PropTypes.number.isRequired, detailedProgressBar: PropTypes.bool.isRequired, showTitle: PropTypes.bool.isRequired, + showMonitored: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired, qualityProfile: PropTypes.object.isRequired, showRelativeDates: PropTypes.bool.isRequired, diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js index 2b9f59846..badfa484c 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js @@ -39,6 +39,7 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) const { detailedProgressBar, showTitle, + showMonitored, showQualityProfile } = posterOptions; @@ -55,6 +56,10 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) heights.push(19); } + if (showMonitored) { + heights.push(19); + } + if (showQualityProfile) { heights.push(19); } @@ -213,6 +218,7 @@ class ArtistIndexPosters extends Component { const { detailedProgressBar, showTitle, + showMonitored, showQualityProfile } = posterOptions; @@ -231,12 +237,16 @@ class ArtistIndexPosters extends Component { posterHeight={posterHeight} detailedProgressBar={detailedProgressBar} showTitle={showTitle} + showMonitored={showMonitored} showQualityProfile={showQualityProfile} showRelativeDates={showRelativeDates} shortDateFormat={shortDateFormat} timeFormat={timeFormat} style={style} - {...artist} + artistId={artist.id} + languageProfileId={artist.languageProfileId} + qualityProfileId={artist.qualityProfileId} + metadataProfileId={artist.metadataProfileId} /> ); } diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js index 6e0a5aa54..5b946b4c6 100644 --- a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js @@ -30,6 +30,7 @@ class ArtistIndexPosterOptionsModalContent extends Component { detailedProgressBar: props.detailedProgressBar, size: props.size, showTitle: props.showTitle, + showMonitored: props.showMonitored, showQualityProfile: props.showQualityProfile }; } @@ -39,6 +40,7 @@ class ArtistIndexPosterOptionsModalContent extends Component { detailedProgressBar, size, showTitle, + showMonitored, showQualityProfile } = this.props; @@ -56,6 +58,10 @@ class ArtistIndexPosterOptionsModalContent extends Component { state.showTitle = showTitle; } + if (showMonitored !== prevProps.showMonitored) { + state.showMonitored = showMonitored; + } + if (showQualityProfile !== prevProps.showQualityProfile) { state.showQualityProfile = showQualityProfile; } @@ -88,6 +94,7 @@ class ArtistIndexPosterOptionsModalContent extends Component { detailedProgressBar, size, showTitle, + showMonitored, showQualityProfile } = this.state; @@ -135,6 +142,18 @@ class ArtistIndexPosterOptionsModalContent extends Component { /> + + Show Monitored + + + + Show Quality Profile @@ -164,6 +183,7 @@ class ArtistIndexPosterOptionsModalContent extends Component { ArtistIndexPosterOptionsModalContent.propTypes = { size: PropTypes.string.isRequired, showTitle: PropTypes.bool.isRequired, + showMonitored: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired, detailedProgressBar: PropTypes.bool.isRequired, onChangePosterOption: PropTypes.func.isRequired, diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.js b/frontend/src/Artist/Index/Table/ArtistIndexTable.js index acd9b2fa7..d49e3a3b9 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTable.js +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.js @@ -10,34 +10,6 @@ import styles from './ArtistIndexTable.css'; class ArtistIndexTable extends Component { - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._table = null; - } - - componentDidUpdate(prevProps) { - const { - columns, - filterKey, - filterValue, - sortKey, - sortDirection - } = this.props; - - if (prevProps.columns !== columns || - prevProps.filterKey !== filterKey || - prevProps.filterValue !== filterValue || - prevProps.sortKey !== sortKey || - prevProps.sortDirection !== sortDirection - ) { - this._table.forceUpdateGrid(); - } - } - // // Control @@ -59,10 +31,6 @@ class ArtistIndexTable extends Component { } } - setTableRef = (ref) => { - this._table = ref; - } - rowRenderer = ({ key, rowIndex, style }) => { const { items, @@ -77,7 +45,10 @@ class ArtistIndexTable extends Component { component={ArtistIndexRow} style={style} columns={columns} - {...artist} + artistId={artist.id} + languageProfileId={artist.languageProfileId} + qualityProfileId={artist.qualityProfileId} + metadataProfileId={artist.metadataProfileId} /> ); } @@ -89,6 +60,8 @@ class ArtistIndexTable extends Component { const { items, columns, + filterKey, + filterValue, sortKey, sortDirection, isSmallScreen, @@ -101,7 +74,6 @@ class ArtistIndexTable extends Component { return ( } + columns={columns} + filterKey={filterKey} + filterValue={filterValue} + sortKey={sortKey} + sortDirection={sortDirection} onRender={onRender} onScroll={onScroll} /> diff --git a/frontend/src/Artist/MoveArtist/MoveArtistModal.css b/frontend/src/Artist/MoveArtist/MoveArtistModal.css new file mode 100644 index 000000000..11f33bef2 --- /dev/null +++ b/frontend/src/Artist/MoveArtist/MoveArtistModal.css @@ -0,0 +1,5 @@ +.doNotMoveButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Artist/MoveArtist/MoveArtistModal.js b/frontend/src/Artist/MoveArtist/MoveArtistModal.js new file mode 100644 index 000000000..8d5fa2d91 --- /dev/null +++ b/frontend/src/Artist/MoveArtist/MoveArtistModal.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +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 styles from './MoveArtistModal.css'; + +function MoveArtistModal(props) { + const { + originalPath, + destinationPath, + destinationRootFolder, + isOpen, + onSavePress, + onMoveArtistPress + } = props; + + if ( + isOpen && + !originalPath && + !destinationPath && + !destinationRootFolder + ) { + console.error('orginalPath and destinationPath OR destinationRootFolder must be provied'); + } + + return ( + + + + Move Files + + + + { + destinationRootFolder ? + `Would you like to move the artist folders to ${destinationPath}'?` : + `Would you like to move the artist files from '${originalPath}' to '${destinationPath}'?` + } + + + + + + + + + + ); +} + +MoveArtistModal.propTypes = { + originalPath: PropTypes.string, + destinationPath: PropTypes.string, + destinationRootFolder: PropTypes.string, + isOpen: PropTypes.bool.isRequired, + onSavePress: PropTypes.func.isRequired, + onMoveArtistPress: PropTypes.func.isRequired +}; + +export default MoveArtistModal; diff --git a/frontend/src/Commands/commandNames.js b/frontend/src/Commands/commandNames.js index e432c42e2..f9ff7103a 100644 --- a/frontend/src/Commands/commandNames.js +++ b/frontend/src/Commands/commandNames.js @@ -10,6 +10,7 @@ export const DOWNLOADED_ALBUMS_SCAN = 'DownloadedAlbumsScan'; export const ALBUM_SEARCH = 'AlbumSearch'; export const INTERACTIVE_IMPORT = 'ManualImport'; export const MISSING_ALBUM_SEARCH = 'MissingAlbumSearch'; +export const MOVE_ARTIST = 'MoveArtist'; export const REFRESH_ARTIST = 'RefreshArtist'; export const RENAME_FILES = 'RenameFiles'; export const RENAME_ARTIST = 'RenameArtist'; diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css index 7fa7f72cb..dc86311ec 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.css +++ b/frontend/src/Components/Form/EnhancedSelectInput.css @@ -31,12 +31,19 @@ .isDisabled { opacity: 0.7; cursor: not-allowed; + pointer-events: all !important; } .dropdownArrowContainer { margin-left: 12px; } +.dropdownArrowContainerDisabled { + composes: dropdownArrowContainer; + + color: $disabledInputColor; +} + .optionsContainer { width: auto; } diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index f3d923895..d5f9b5d0c 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.js +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -289,6 +289,7 @@ class EnhancedSelectInput extends Component { hasWarning && styles.hasWarning, isDisabled && disabledClassName )} + isDisabled={isDisabled} onBlur={this.onBlur} onKeyDown={this.onKeyDown} onPress={this.onPress} @@ -296,11 +297,17 @@ class EnhancedSelectInput extends Component { {selectedOption ? selectedOption.value : null} -
+
diff --git a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css index aab9f1b7d..6b8b73af9 100644 --- a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css +++ b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css @@ -1,3 +1,7 @@ .selectedValue { flex: 1 1 auto; } + +.isDisabled { + color: $disabledInputColor; +} diff --git a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js index 2343fedc2..c40ee93c1 100644 --- a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js +++ b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js @@ -1,15 +1,21 @@ import PropTypes from 'prop-types'; import React from 'react'; +import classNames from 'classnames'; import styles from './EnhancedSelectInputSelectedValue.css'; function EnhancedSelectInputSelectedValue(props) { const { className, - children + children, + isDisabled } = props; return ( -
+
{children}
); @@ -17,11 +23,13 @@ function EnhancedSelectInputSelectedValue(props) { EnhancedSelectInputSelectedValue.propTypes = { className: PropTypes.string.isRequired, - children: PropTypes.node + children: PropTypes.node, + isDisabled: PropTypes.bool.isRequired }; EnhancedSelectInputSelectedValue.defaultProps = { - className: styles.selectedValue + className: styles.selectedValue, + isDisabled: false }; export default EnhancedSelectInputSelectedValue; diff --git a/frontend/src/Components/Modal/Modal.js b/frontend/src/Components/Modal/Modal.js index e4e221076..1dbe4a793 100644 --- a/frontend/src/Components/Modal/Modal.js +++ b/frontend/src/Components/Modal/Modal.js @@ -109,8 +109,17 @@ class Modal extends Component { } onBackdropEndPress = (event) => { - if (this._isBackdropPressed && this._isBackdropTarget(event)) { - this.props.onModalClose(); + const { + closeOnBackgroundClick, + onModalClose + } = this.props; + + if ( + this._isBackdropPressed && + this._isBackdropTarget(event) && + closeOnBackgroundClick + ) { + onModalClose(); } this._isBackdropPressed = false; @@ -187,13 +196,15 @@ Modal.propTypes = { size: PropTypes.oneOf(sizes.all), children: PropTypes.node, isOpen: PropTypes.bool.isRequired, + closeOnBackgroundClick: PropTypes.bool.isRequired, onModalClose: PropTypes.func.isRequired }; Modal.defaultProps = { className: styles.modal, backdropClassName: styles.modalBackdrop, - size: sizes.LARGE + size: sizes.LARGE, + closeOnBackgroundClick: true }; export default Modal; diff --git a/frontend/src/Components/Modal/ModalContent.js b/frontend/src/Components/Modal/ModalContent.js index cc165dda2..655046fe4 100644 --- a/frontend/src/Components/Modal/ModalContent.js +++ b/frontend/src/Components/Modal/ModalContent.js @@ -9,6 +9,7 @@ function ModalContent(props) { const { className, children, + showCloseButton, onModalClose, ...otherProps } = props; @@ -18,15 +19,18 @@ function ModalContent(props) { className={className} {...otherProps} > - - - + { + showCloseButton && + + + + } {children}
@@ -36,11 +40,13 @@ function ModalContent(props) { ModalContent.propTypes = { className: PropTypes.string, children: PropTypes.node, + showCloseButton: PropTypes.bool.isRequired, onModalClose: PropTypes.func.isRequired }; ModalContent.defaultProps = { - className: styles.modalContent + className: styles.modalContent, + showCloseButton: true }; export default ModalContent; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 50ddc3ae7..b22665a4f 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -482,7 +482,7 @@ class PageSidebar extends Component { key={child.to} title={child.title} to={child.to} - isActive={pathname === child.to} + isActive={pathname.startsWith(child.to)} isParentItem={false} isChildItem={true} statusComponent={child.statusComponent} diff --git a/frontend/src/Components/Table/VirtualTable.js b/frontend/src/Components/Table/VirtualTable.js index df23d3ff7..ca9b49dd8 100644 --- a/frontend/src/Components/Table/VirtualTable.js +++ b/frontend/src/Components/Table/VirtualTable.js @@ -44,7 +44,6 @@ class VirtualTable extends Component { }; this._isInitialized = false; - this._table = null; } componentDidMount() { @@ -58,18 +57,9 @@ class VirtualTable extends Component { return this.props.items[index]; } - setTableRef = (ref) => { - this._table = ref; - } - - forceUpdateGrid = () => { - this._table.recomputeGridSize(); - } - scrollToRow = (rowIndex) => { const scrollTop = (rowIndex + 1) * ROW_HEIGHT + 20; - // this._table.scrollToCell({ columnIndex: 0, rowIndex }); this.props.onScroll({ scrollTop }); } @@ -124,7 +114,6 @@ class VirtualTable extends Component { {header} { + this.props.onFilterExistingFilesChange(value !== filterExistingFilesOptions.ALL); + } + onImportModeChange = ({ value }) => { this.props.onImportModeChange(value); } @@ -155,6 +168,8 @@ class InteractiveImportModalContent extends Component { render() { const { downloadId, + showFilterExistingFiles, + filterExistingFiles, title, folder, isFetching, @@ -205,7 +220,45 @@ class InteractiveImportModalContent extends Component { } { - isPopulated && !!items.length && + isPopulated && showFilterExistingFiles && !isFetching && +
+ + + + +
+ { + filterExistingFiles ? 'Unmapped Files Only' : 'All Files' + } +
+
+ + + + All Files + + + + Unmapped Files Only + + +
+
+ } + + { + isPopulated && !!items.length && !isFetching && !isFetching && @@ -303,6 +356,8 @@ class InteractiveImportModalContent extends Component { InteractiveImportModalContent.propTypes = { downloadId: PropTypes.string, + showFilterExistingFiles: PropTypes.bool.isRequired, + filterExistingFiles: PropTypes.bool.isRequired, importMode: PropTypes.string.isRequired, title: PropTypes.string, folder: PropTypes.string, @@ -314,12 +369,14 @@ InteractiveImportModalContent.propTypes = { sortDirection: PropTypes.string, interactiveImportErrorMessage: PropTypes.string, onSortPress: PropTypes.func.isRequired, + onFilterExistingFilesChange: PropTypes.func.isRequired, onImportModeChange: PropTypes.func.isRequired, onImportSelectedPress: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; InteractiveImportModalContent.defaultProps = { + showFilterExistingFiles: false, importMode: 'move' }; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index fd07437f1..0be91d2d4 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -35,7 +35,8 @@ class InteractiveImportModalContentConnector extends Component { super(props, context); this.state = { - interactiveImportErrorMessage: null + interactiveImportErrorMessage: null, + filterExistingFiles: true }; } @@ -45,7 +46,34 @@ class InteractiveImportModalContentConnector extends Component { folder } = this.props; - this.props.fetchInteractiveImportItems({ downloadId, folder }); + const { + filterExistingFiles + } = this.state; + + this.props.fetchInteractiveImportItems({ + downloadId, + folder, + filterExistingFiles + }); + } + + componentDidUpdate(prevProps, prevState) { + const { + filterExistingFiles + } = this.state; + + if (prevState.filterExistingFiles !== filterExistingFiles) { + const { + downloadId, + folder + } = this.props; + + this.props.fetchInteractiveImportItems({ + downloadId, + folder, + filterExistingFiles + }); + } } componentWillUnmount() { @@ -59,6 +87,10 @@ class InteractiveImportModalContentConnector extends Component { this.props.setInteractiveImportSort({ sortKey, sortDirection }); } + onFilterExistingFilesChange = (filterExistingFiles) => { + this.setState({ filterExistingFiles }); + } + onImportModeChange = (importMode) => { this.props.setInteractiveImportMode({ importMode }); } @@ -122,11 +154,18 @@ class InteractiveImportModalContentConnector extends Component { // Render render() { + const { + interactiveImportErrorMessage, + filterExistingFiles + } = this.state; + return ( @@ -137,6 +176,7 @@ class InteractiveImportModalContentConnector extends Component { InteractiveImportModalContentConnector.propTypes = { downloadId: PropTypes.string, folder: PropTypes.string, + filterExistingFiles: PropTypes.bool.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, fetchInteractiveImportItems: PropTypes.func.isRequired, setInteractiveImportSort: PropTypes.func.isRequired, @@ -146,6 +186,10 @@ InteractiveImportModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; +InteractiveImportModalContentConnector.defaultProps = { + filterExistingFiles: true +}; + export default connectSection( createMapStateToProps, mapDispatchToProps, diff --git a/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js b/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js index 519ea930d..06e792cf3 100644 --- a/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js +++ b/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js @@ -15,6 +15,12 @@ import TableBody from 'Components/Table/TableBody'; import SelectTrackRow from './SelectTrackRow'; const columns = [ + { + name: 'mediumNumber', + label: 'Medium', + isSortable: true, + isVisible: true + }, { name: 'trackNumber', label: '#', @@ -127,7 +133,8 @@ class SelectTrackModalContent extends Component { + + {mediumNumber} + + {trackNumber} @@ -53,6 +58,7 @@ class SelectTrackRow extends Component { SelectTrackRow.propTypes = { id: PropTypes.number.isRequired, + mediumNumber: PropTypes.number.isRequired, trackNumber: PropTypes.number.isRequired, title: PropTypes.string.isRequired, isSelected: PropTypes.bool, diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js index 21d33bea6..ed4090c3f 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -264,7 +264,7 @@ class MediaManagement extends Component { advancedSettings={advancedSettings} isAdvanced={true} > - File chmod mask + File chmod mode - Folder chmod mask + Folder chmod mode { + const newPayload = { + ...payload + }; + + if (payload.moveFiles) { + newPayload.queryParams = { + moveFiles: true + }; + } + + delete newPayload.moveFiles; + + return newPayload; +}); + +export const deleteArtist = createThunk(DELETE_ARTIST, (payload) => { + return { + ...payload, + queryParams: { + deleteFiles: payload.deleteFiles + } + }; +}); + export const toggleArtistMonitored = createThunk(TOGGLE_ARTIST_MONITORED); export const toggleAlbumMonitored = createThunk(TOGGLE_ALBUM_MONITORED); @@ -58,20 +81,25 @@ export const setArtistValue = createAction(SET_ARTIST_VALUE, (payload) => { }; }); +// +// Helpers + +function getSaveAjaxOptions({ ajaxOptions, payload }) { + if (payload.moveFolder) { + ajaxOptions.url = `${ajaxOptions.url}?moveFolder=true`; + } + + return ajaxOptions; +} + // // Action Handlers export const actionHandlers = handleThunks({ [FETCH_ARTIST]: createFetchHandler(section, '/artist'), - - [SAVE_ARTIST]: createSaveProviderHandler( - section, '/artist'), - - [DELETE_ARTIST]: createRemoveItemHandler( - section, - '/artist' - ), + [SAVE_ARTIST]: createSaveProviderHandler(section, '/artist', { getAjaxOptions: getSaveAjaxOptions }), + [DELETE_ARTIST]: createRemoveItemHandler(section, '/artist'), [TOGGLE_ARTIST_MONITORED]: (getState, payload, dispatch) => { const { @@ -115,7 +143,7 @@ export const actionHandlers = handleThunks({ }); }, - [TOGGLE_ALBUM_MONITORED]: (getState, payload, dispatch) => { + [TOGGLE_ALBUM_MONITORED]: function(getState, payload, dispatch) { const { artistId: id, seasonNumber, diff --git a/frontend/src/Store/Actions/artistEditorActions.js b/frontend/src/Store/Actions/artistEditorActions.js index 4da674087..b8f2dcc20 100644 --- a/frontend/src/Store/Actions/artistEditorActions.js +++ b/frontend/src/Store/Actions/artistEditorActions.js @@ -112,7 +112,7 @@ export const actionHandlers = handleThunks({ }); promise.done(() => { - // SignaR will take care of removing the serires from the collection + // SignalR will take care of removing the artist from the collection dispatch(set({ section, diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js index e3d085b66..c8d978fae 100644 --- a/frontend/src/Store/Actions/artistIndexActions.js +++ b/frontend/src/Store/Actions/artistIndexActions.js @@ -28,6 +28,7 @@ export const defaultState = { detailedProgressBar: false, size: 'large', showTitle: false, + showMonitored: true, showQualityProfile: true }, @@ -35,12 +36,14 @@ export const defaultState = { detailedProgressBar: false, size: 'large', showTitle: false, + showMonitored: true, showQualityProfile: true }, overviewOptions: { detailedProgressBar: false, size: 'medium', + showMonitored: true, showNetwork: true, showQualityProfile: true, showPreviousAiring: false, diff --git a/frontend/src/Store/Actions/importArtistActions.js b/frontend/src/Store/Actions/importArtistActions.js index ad4fbf0f3..5d327644d 100644 --- a/frontend/src/Store/Actions/importArtistActions.js +++ b/frontend/src/Store/Actions/importArtistActions.js @@ -2,6 +2,7 @@ import _ from 'lodash'; import $ from 'jquery'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; +import createAjaxRequest from 'Utilities/createAjaxRequest'; import getSectionState from 'Utilities/State/getSectionState'; import updateSectionState from 'Utilities/State/updateSectionState'; import getNewArtist from 'Utilities/Artist/getNewArtist'; @@ -15,14 +16,14 @@ import { fetchRootFolders } from './rootFolderActions'; export const section = 'importArtist'; let concurrentLookups = 0; +let abortCurrentLookup = null; +const queue = []; // // State export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, + isLookingUpArtist: false, isImporting: false, isImported: false, importError: null, @@ -34,9 +35,10 @@ export const defaultState = { export const QUEUE_LOOKUP_ARTIST = 'importArtist/queueLookupArtist'; export const START_LOOKUP_ARTIST = 'importArtist/startLookupArtist'; -export const CLEAR_IMPORT_ARTIST = 'importArtist/importArtist'; -export const SET_IMPORT_ARTIST_VALUE = 'importArtist/clearImportArtist'; -export const IMPORT_ARTIST = 'importArtist/setImportArtistValue'; +export const CANCEL_LOOKUP_ARTIST = 'importArtist/cancelLookupArtist'; +export const CLEAR_IMPORT_ARTIST = 'importArtist/clearImportArtist'; +export const SET_IMPORT_ARTIST_VALUE = 'importArtist/setImportArtistValue'; +export const IMPORT_ARTIST = 'importArtist/importArtist'; // // Action Creators @@ -45,10 +47,10 @@ export const queueLookupArtist = createThunk(QUEUE_LOOKUP_ARTIST); export const startLookupArtist = createThunk(START_LOOKUP_ARTIST); export const importArtist = createThunk(IMPORT_ARTIST); export const clearImportArtist = createAction(CLEAR_IMPORT_ARTIST); +export const cancelLookupArtist = createAction(CANCEL_LOOKUP_ARTIST); export const setImportArtistValue = createAction(SET_IMPORT_ARTIST_VALUE, (payload) => { return { - section, ...payload }; @@ -63,7 +65,8 @@ export const actionHandlers = handleThunks({ const { name, path, - term + term, + topOfQueue = false } = payload; const state = getState().importArtist; @@ -84,8 +87,20 @@ export const actionHandlers = handleThunks({ items: [] })); + const itemIndex = queue.indexOf(item.id); + + if (itemIndex >= 0) { + queue.splice(itemIndex, 1); + } + + if (topOfQueue) { + queue.unshift(item.id); + } else { + queue.push(item.id); + } + if (term && term.length > 2) { - dispatch(startLookupArtist()); + dispatch(startLookupArtist({ start: true })); } }, @@ -95,13 +110,27 @@ export const actionHandlers = handleThunks({ } const state = getState().importArtist; - const queued = _.find(state.items, { queued: true }); - if (!queued) { + const { + isLookingUpArtist, + items + } = state; + + const queueId = queue[0]; + + if (payload.start && !isLookingUpArtist) { + dispatch(set({ section, isLookingUpArtist: true })); + } else if (!isLookingUpArtist) { + return; + } else if (!queueId) { + dispatch(set({ section, isLookingUpArtist: false })); return; } concurrentLookups++; + queue.splice(0, 1); + + const queued = items.find((i) => i.id === queueId); dispatch(updateItem({ section, @@ -109,14 +138,16 @@ export const actionHandlers = handleThunks({ isFetching: true })); - const promise = $.ajax({ + const { request, abortRequest } = createAjaxRequest({ url: '/artist/lookup', data: { term: queued.term } }); - promise.done((data) => { + abortCurrentLookup = abortRequest; + + request.done((data) => { dispatch(updateItem({ section, id: queued.id, @@ -125,23 +156,26 @@ export const actionHandlers = handleThunks({ error: null, items: data, queued: false, - selectedArtist: queued.selectedArtist || data[0] + selectedArtist: queued.selectedArtist || data[0], + updateOnly: true })); }); - promise.fail((xhr) => { + request.fail((xhr) => { dispatch(updateItem({ section, id: queued.id, isFetching: false, isPopulated: false, error: xhr, - queued: false + queued: false, + updateOnly: true })); }); - promise.always(() => { + request.always(() => { concurrentLookups--; + dispatch(startLookupArtist()); }); }, @@ -159,7 +193,7 @@ export const actionHandlers = handleThunks({ // Make sure we have a selected artist and // the same artist hasn't been added yet. - if (selectedArtist && !_.some(acc, { tvdbId: selectedArtist.tvdbId })) { + if (selectedArtist && !_.some(acc, { foreignArtistId: selectedArtist.foreignArtistId })) { const newArtist = getNewArtist(_.cloneDeep(selectedArtist), item); newArtist.path = item.path; @@ -216,7 +250,19 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ + [CANCEL_LOOKUP_ARTIST]: function(state) { + return Object.assign({}, state, { isLookingUpArtist: false }); + }, + [CLEAR_IMPORT_ARTIST]: function(state) { + if (abortCurrentLookup) { + abortCurrentLookup(); + + abortCurrentLookup = null; + } + + queue.splice(0, queue.length); + return Object.assign({}, state, defaultState); }, diff --git a/frontend/src/Store/Actions/tagActions.js b/frontend/src/Store/Actions/tagActions.js index 3f5b708fe..29c61cf61 100644 --- a/frontend/src/Store/Actions/tagActions.js +++ b/frontend/src/Store/Actions/tagActions.js @@ -35,24 +35,22 @@ export const addTag = createThunk(ADD_TAG); // Action Handlers export const actionHandlers = handleThunks({ - [FETCH_TAGS]: createFetchHandler('tags', '/tag'), - - [ADD_TAG]: function(payload) { - return (dispatch, getState) => { - const promise = $.ajax({ - url: '/tag', - method: 'POST', - data: JSON.stringify(payload.tag) - }); - - promise.done((data) => { - const tags = getState().tags.items.slice(); - tags.push(data); - - dispatch(update({ section: 'tags', data: tags })); - payload.onTagCreated(data); - }); - }; + [FETCH_TAGS]: createFetchHandler(section, '/tag'), + + [ADD_TAG]: function(getState, payload, dispatch) { + const promise = $.ajax({ + url: '/tag', + method: 'POST', + data: JSON.stringify(payload.tag) + }); + + promise.done((data) => { + const tags = getState().tags.items.slice(); + tags.push(data); + + dispatch(update({ section, data: tags })); + payload.onTagCreated(data); + }); } }); diff --git a/frontend/src/Store/thunks.js b/frontend/src/Store/thunks.js index f46ee3a23..6daa843f4 100644 --- a/frontend/src/Store/thunks.js +++ b/frontend/src/Store/thunks.js @@ -1,12 +1,16 @@ const thunks = {}; -export function createThunk(type) { +function identity(payload) { + return payload; +} + +export function createThunk(type, identityFunction = identity) { return function(payload = {}) { return function(dispatch, getState) { const thunk = thunks[type]; if (thunk) { - return thunk(getState, payload, dispatch); + return thunk(getState, identityFunction(payload), dispatch); } throw Error(`Thunk handler has not been registered for ${type}`); @@ -21,4 +25,3 @@ export function handleThunks(handlers) { thunks[type] = handlers[type]; }); } - diff --git a/frontend/src/Styles/Variables/colors.js b/frontend/src/Styles/Variables/colors.js index 402358e66..ed168cdde 100644 --- a/frontend/src/Styles/Variables/colors.js +++ b/frontend/src/Styles/Variables/colors.js @@ -16,6 +16,7 @@ module.exports = { sonarrBlue: '#00A65B', helpTextColor: '#909293', gray: '#adadad', + disabledInputColor: '#808080', // Theme Colors diff --git a/src/Lidarr.Api.V1/Artist/ArtistEditorModule.cs b/src/Lidarr.Api.V1/Artist/ArtistEditorModule.cs index f387db154..0b5bc5343 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistEditorModule.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistEditorModule.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; +using System.Linq; using Nancy; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Commands; using Lidarr.Http.Extensions; namespace Lidarr.Api.V1.Artist @@ -9,11 +12,13 @@ namespace Lidarr.Api.V1.Artist public class ArtistEditorModule : LidarrV1Module { private readonly IArtistService _artistService; + private readonly IManageCommandQueue _commandQueueManager; - public ArtistEditorModule(IArtistService artistService) + public ArtistEditorModule(IArtistService artistService, IManageCommandQueue commandQueueManager) : base("/artist/editor") { _artistService = artistService; + _commandQueueManager = commandQueueManager; Put["/"] = artist => SaveAll(); Delete["/"] = artist => DeleteArtist(); } @@ -22,6 +27,7 @@ namespace Lidarr.Api.V1.Artist { var resource = Request.Body.FromJson(); var artistToUpdate = _artistService.GetArtists(resource.ArtistIds); + var artistToMove = new List(); foreach (var artist in artistToUpdate) { @@ -53,6 +59,12 @@ namespace Lidarr.Api.V1.Artist if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) { artist.RootFolderPath = resource.RootFolderPath; + artistToMove.Add(new BulkMoveArtist + { + ArtistId = artist.Id, + SourcePath = artist.Path + }); + } if (resource.Tags != null) @@ -75,6 +87,15 @@ namespace Lidarr.Api.V1.Artist } } + if (resource.MoveFiles && artistToMove.Any()) + { + _commandQueueManager.Push(new BulkMoveArtistCommand + { + DestinationRootFolder = resource.RootFolderPath, + Artist = artistToMove + }); + } + return _artistService.UpdateArtists(artistToUpdate) .ToResource() .AsResponse(HttpStatusCode.Accepted); diff --git a/src/Lidarr.Api.V1/Artist/ArtistEditorResource.cs b/src/Lidarr.Api.V1/Artist/ArtistEditorResource.cs index d50352cd8..30920047b 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistEditorResource.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistEditorResource.cs @@ -14,6 +14,7 @@ namespace Lidarr.Api.V1.Artist public string RootFolderPath { get; set; } public List Tags { get; set; } public ApplyTags ApplyTags { get; set; } + public bool MoveFiles { get; set; } } public enum ApplyTags diff --git a/src/Lidarr.Api.V1/Artist/ArtistModule.cs b/src/Lidarr.Api.V1/Artist/ArtistModule.cs index af3ef571d..596d08e05 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistModule.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistModule.cs @@ -7,9 +7,11 @@ using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ArtistStats; using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Commands; using NzbDrone.Core.Music.Events; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -35,12 +37,14 @@ namespace Lidarr.Api.V1.Artist private readonly IAddArtistService _addArtistService; private readonly IArtistStatisticsService _artistStatisticsService; private readonly IMapCoversToLocal _coverMapper; + private readonly IManageCommandQueue _commandQueueManager; public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster, IArtistService artistService, IAddArtistService addArtistService, IArtistStatisticsService artistStatisticsService, IMapCoversToLocal coverMapper, + IManageCommandQueue commandQueueManager, RootFolderValidator rootFolderValidator, ArtistPathValidator artistPathValidator, ArtistExistsValidator artistExistsValidator, @@ -57,6 +61,7 @@ namespace Lidarr.Api.V1.Artist _artistStatisticsService = artistStatisticsService; _coverMapper = coverMapper; + _commandQueueManager = commandQueueManager; GetResourceAll = AllArtists; GetResourceById = GetArtist; @@ -127,7 +132,24 @@ namespace Lidarr.Api.V1.Artist private void UpdateArtist(ArtistResource artistResource) { - var model = artistResource.ToModel(_artistService.GetArtist(artistResource.Id)); + var moveFiles = Request.GetBooleanQueryParameter("moveFiles"); + var artist = _artistService.GetArtist(artistResource.Id); + + if (moveFiles) + { + var sourcePath = artist.Path; + var destinationPath = artistResource.Path; + + _commandQueueManager.Push(new MoveArtistCommand + { + ArtistId = artist.Id, + SourcePath = sourcePath, + DestinationPath = destinationPath, + Trigger = CommandTrigger.Manual + }); + } + + var model = artistResource.ToModel(artist); _artistService.UpdateArtist(model); diff --git a/src/Lidarr.Api.V1/History/HistoryModule.cs b/src/Lidarr.Api.V1/History/HistoryModule.cs index e750349e2..9e72bc9de 100644 --- a/src/Lidarr.Api.V1/History/HistoryModule.cs +++ b/src/Lidarr.Api.V1/History/HistoryModule.cs @@ -55,10 +55,8 @@ namespace Lidarr.Api.V1.History if (model.Artist != null) { - resource.QualityCutoffNotMet = _upgradableSpecification.CutoffNotMet(model.Artist.Profile.Value, - model.Artist.LanguageProfile, - model.Quality, - model.Language); + resource.QualityCutoffNotMet = _upgradableSpecification.QualityCutoffNotMet(model.Artist.Profile.Value, model.Quality); + resource.LanguageCutoffNotMet = _upgradableSpecification.LanguageCutoffNotMet(model.Artist.LanguageProfile, model.Language); } return resource; diff --git a/src/Lidarr.Api.V1/History/HistoryResource.cs b/src/Lidarr.Api.V1/History/HistoryResource.cs index a4a38ed6e..27c168b4b 100644 --- a/src/Lidarr.Api.V1/History/HistoryResource.cs +++ b/src/Lidarr.Api.V1/History/HistoryResource.cs @@ -19,6 +19,7 @@ namespace Lidarr.Api.V1.History public Language Language { get; set; } public QualityModel Quality { get; set; } public bool QualityCutoffNotMet { get; set; } + public bool LanguageCutoffNotMet { get; set; } public DateTime Date { get; set; } public string DownloadId { get; set; } diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs index 5d5f9f088..c59b259ef 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs @@ -3,6 +3,8 @@ using System.Linq; using NzbDrone.Core.MediaFiles.TrackImport.Manual; using NzbDrone.Core.Qualities; using Lidarr.Http; +using Lidarr.Http.Extensions; + namespace Lidarr.Api.V1.ManualImport { @@ -22,8 +24,9 @@ namespace Lidarr.Api.V1.ManualImport { var folder = (string)Request.Query.folder; var downloadId = (string)Request.Query.downloadId; + var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true); - return _manualImportService.GetMediaFiles(folder, downloadId).ToResource().Select(AddQualityWeight).ToList(); + return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList(); } private ManualImportResource AddQualityWeight(ManualImportResource item) diff --git a/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs b/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs index eabb60f73..fb69f1e2d 100644 --- a/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs +++ b/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs @@ -22,6 +22,8 @@ namespace Lidarr.Api.V1.TrackFiles public MediaInfoResource MediaInfo { get; set; } public bool QualityCutoffNotMet { get; set; } + public bool LanguageCutoffNotMet { get; set; } + } public static class TrackFileResourceMapper @@ -67,11 +69,9 @@ namespace Lidarr.Api.V1.TrackFiles Language = model.Language, Quality = model.Quality, MediaInfo = model.MediaInfo.ToResource(model.SceneName), - - QualityCutoffNotMet = upgradableSpecification.CutoffNotMet(artist.Profile.Value, - artist.LanguageProfile.Value, - model.Quality, - model.Language) + + QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(artist.Profile.Value, model.Quality), + LanguageCutoffNotMet = upgradableSpecification.LanguageCutoffNotMet(artist.LanguageProfile.Value, model.Language) }; } } diff --git a/src/NzbDrone.Core.Test/MusicTests/MoveArtistServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/MoveArtistServiceFixture.cs index d7dfa6c5a..05a8b593f 100644 --- a/src/NzbDrone.Core.Test/MusicTests/MoveArtistServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/MoveArtistServiceFixture.cs @@ -1,4 +1,6 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.Linq; using FizzWare.NBuilder; using Moq; using NUnit.Framework; @@ -16,6 +18,7 @@ namespace NzbDrone.Core.Test.MusicTests { private Artist _artist; private MoveArtistCommand _command; + private BulkMoveArtistCommand _bulkCommand; [SetUp] public void Setup() @@ -31,6 +34,19 @@ namespace NzbDrone.Core.Test.MusicTests DestinationPath = @"C:\Test\Music2\Artist".AsOsAgnostic() }; + _bulkCommand = new BulkMoveArtistCommand + { + Artist = new List + { + new BulkMoveArtist + { + ArtistId = 1, + SourcePath = @"C:\Test\Music\Artist".AsOsAgnostic() + } + }, + DestinationRootFolder = @"C:\Test\Music2".AsOsAgnostic() + }; + Mocker.GetMock() .Setup(s => s.GetArtist(It.IsAny())) .Returns(_artist); @@ -48,52 +64,52 @@ namespace NzbDrone.Core.Test.MusicTests { GivenFailedMove(); - Assert.Throws(() => Subject.Execute(_command)); + Subject.Execute(_command); ExceptionVerification.ExpectedErrors(1); } [Test] - public void should_no_update_artist_path_on_error() + public void should_revert_artist_path_on_error() { GivenFailedMove(); - Assert.Throws(() => Subject.Execute(_command)); + Subject.Execute(_command); ExceptionVerification.ExpectedErrors(1); Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.IsAny()), Times.Never()); + .Verify(v => v.UpdateArtist(It.IsAny()), Times.Once()); } [Test] - public void should_build_new_path_when_root_folder_is_provided() + public void should_use_destination_path() { - _command.DestinationPath = null; - _command.DestinationRootFolder = @"C:\Test\Music3".AsOsAgnostic(); - - var expectedPath = @"C:\Test\Music3\Artist".AsOsAgnostic(); - - Mocker.GetMock() - .Setup(s => s.GetArtistFolder(It.IsAny(), null)) - .Returns("Artist"); Subject.Execute(_command); - Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.Is(s => s.Path == expectedPath)), Times.Once()); + Mocker.GetMock() + .Verify(v => v.TransferFolder(_command.SourcePath, _command.DestinationPath, TransferMode.Move, It.IsAny()), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.GetArtistFolder(It.IsAny(), null), Times.Never()); } [Test] - public void should_use_destination_path_if_destination_root_folder_is_blank() + public void should_build_new_path_when_root_folder_is_provided() { - Subject.Execute(_command); + var artistFolder = "Artist"; + var expectedPath = Path.Combine(_bulkCommand.DestinationRootFolder, artistFolder); - Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.Is(s => s.Path == _command.DestinationPath)), Times.Once()); Mocker.GetMock() - .Verify(v => v.GetArtistFolder(It.IsAny(), null), Times.Never()); + .Setup(s => s.GetArtistFolder(It.IsAny(), null)) + .Returns(artistFolder); + + Subject.Execute(_bulkCommand); + + Mocker.GetMock() + .Verify(v => v.TransferFolder(_bulkCommand.Artist.First().SourcePath, expectedPath, TransferMode.Move, It.IsAny()), Times.Once()); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs index cd6d9d3e9..278844294 100644 --- a/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs @@ -9,6 +9,8 @@ namespace NzbDrone.Core.DecisionEngine public interface IUpgradableSpecification { bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage); + bool QualityCutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null); + bool LanguageCutoffNotMet(LanguageProfile languageProfile, Language currentLanguage); bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null); bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality); } @@ -68,29 +70,46 @@ namespace NzbDrone.Core.DecisionEngine return true; } - public bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null) + public bool QualityCutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) { - var languageCompare = new LanguageComparer(languageProfile).Compare(currentLanguage, languageProfile.Cutoff); var qualityCompare = new QualityModelComparer(profile).Compare(currentQuality.Quality.Id, profile.Cutoff); - // If we can upgrade the language (it is not the cutoff) then doesn't matter the quality we can always get same quality with prefered language - if (languageCompare < 0) + if (qualityCompare < 0) { return true; } - if (qualityCompare >= 0) + if (qualityCompare == 0 && newQuality != null && IsRevisionUpgrade(currentQuality, newQuality)) { - if (newQuality != null && IsRevisionUpgrade(currentQuality, newQuality)) - { - return true; - } + return true; + } - _logger.Debug("Existing item meets cut-off. skipping."); - return false; + return false; + } + + public bool LanguageCutoffNotMet(LanguageProfile languageProfile, Language currentLanguage) + { + var languageCompare = new LanguageComparer(languageProfile).Compare(currentLanguage, languageProfile.Cutoff); + + return languageCompare < 0; + } + + public bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null) + { + // If we can upgrade the language (it is not the cutoff) then doesn't matter the quality we can always get same quality with prefered language + if (LanguageCutoffNotMet(languageProfile, currentLanguage)) + { + return true; } - return true; + if (QualityCutoffNotMet(profile, currentQuality, newQuality)) + { + return true; + } + + _logger.Debug("Existing item meets cut-off. skipping."); + + return false; } public bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality) diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs index af86d061b..26b1077be 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.MediaFiles.Commands @@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.Commands public RenameArtistCommand() { + ArtistIds = new List(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index acfd2afbd..7859f54f6 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -19,6 +19,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport { List GetImportDecisions(List musicFiles, Artist artist); List GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo); + List GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo, bool filterExistingFiles); + } public class ImportDecisionMaker : IMakeImportDecision @@ -52,14 +54,19 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public List GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo) { - var newFiles = _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist); + return GetImportDecisions(musicFiles, artist, folderInfo, false); + } + + public List GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo, bool filterExistingFiles) + { + var files = filterExistingFiles ? _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist) : musicFiles.ToList(); - _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, musicFiles.Count()); + _logger.Debug("Analyzing {0}/{1} files.", files.Count, musicFiles.Count); var shouldUseFolderName = ShouldUseFolderName(musicFiles, artist, folderInfo); var decisions = new List(); - foreach (var file in newFiles) + foreach (var file in files) { decisions.AddIfNotNull(GetDecision(file, artist, folderInfo, shouldUseFolderName)); } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index e7f53b398..fba198b1d 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { public interface IManualImportService { - List GetMediaFiles(string path, string downloadId); + List GetMediaFiles(string path, string downloadId, bool filterExistingFiles); } public class ManualImportService : IExecute, IManualImportService @@ -68,7 +68,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual _logger = logger; } - public List GetMediaFiles(string path, string downloadId) + public List GetMediaFiles(string path, string downloadId, bool filterExistingFiles) { if (downloadId.IsNotNullOrWhiteSpace()) { @@ -92,10 +92,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual return new List { ProcessFile(path, downloadId) }; } - return ProcessFolder(path, downloadId); + return ProcessFolder(path, downloadId, filterExistingFiles); } - private List ProcessFolder(string folder, string downloadId) + private List ProcessFolder(string folder, string downloadId, bool filterExistingFiles) { var directoryInfo = new DirectoryInfo(folder); var artist = _parsingService.GetArtist(directoryInfo.Name); @@ -115,7 +115,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var folderInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name); var artistFiles = _diskScanService.GetAudioFiles(folder).ToList(); - var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, folderInfo); + var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, folderInfo, filterExistingFiles); return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList(); } diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs index 9bd021192..f25c08ab2 100644 --- a/src/NzbDrone.Core/Music/ArtistService.cs +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -140,8 +140,11 @@ namespace NzbDrone.Core.Music _logger.Trace("Updating: {0}", s.Name); if (!s.RootFolderPath.IsNullOrWhiteSpace()) { - var folderName = new DirectoryInfo(s.Path).Name; - s.Path = Path.Combine(s.RootFolderPath, folderName); + // Build the artist folder name instead of using the existing folder name. + // This may lead to folder name changes, but consistent with adding a new artist. + + s.Path = Path.Combine(s.RootFolderPath, _fileNameBuilder.GetArtistFolder(s)); + _logger.Trace("Changing path for {0} to {1}", s.Name, s.Path); } diff --git a/src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs b/src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs new file mode 100644 index 000000000..52a4cfafd --- /dev/null +++ b/src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Music.Commands +{ + public class BulkMoveArtistCommand : Command + { + public List Artist { get; set; } + public string DestinationRootFolder { get; set; } + + public override bool SendUpdatesToClient => true; + } + + public class BulkMoveArtist + { + public int ArtistId { get; set; } + public string SourcePath { get; set; } + } +} diff --git a/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs b/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs index 78dc161e0..4ece88c3b 100644 --- a/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs +++ b/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Music.Commands { @@ -7,6 +7,7 @@ namespace NzbDrone.Core.Music.Commands public int ArtistId { get; set; } public string SourcePath { get; set; } public string DestinationPath { get; set; } - public string DestinationRootFolder { get; set; } + + public override bool SendUpdatesToClient => true; } } diff --git a/src/NzbDrone.Core/Music/MoveArtistService.cs b/src/NzbDrone.Core/Music/MoveArtistService.cs index b1f4c52a1..9ae6cc2cf 100644 --- a/src/NzbDrone.Core/Music/MoveArtistService.cs +++ b/src/NzbDrone.Core/Music/MoveArtistService.cs @@ -1,7 +1,6 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -11,7 +10,7 @@ using NzbDrone.Core.Music.Events; namespace NzbDrone.Core.Music { - public class MoveArtistService : IExecute + public class MoveArtistService : IExecute, IExecute { private readonly IArtistService _artistService; private readonly IBuildFileNames _filenameBuilder; @@ -32,38 +31,56 @@ namespace NzbDrone.Core.Music _logger = logger; } - public void Execute(MoveArtistCommand message) + private void MoveSingleArtist(Artist artist, string sourcePath, string destinationPath) { - var artist = _artistService.GetArtist(message.ArtistId); - var source = message.SourcePath; - var destination = message.DestinationPath; - - if (!message.DestinationRootFolder.IsNullOrWhiteSpace()) - { - _logger.Debug("Buiding destination path using root folder: {0} and the artist name", message.DestinationRootFolder); - destination = Path.Combine(message.DestinationRootFolder, _filenameBuilder.GetArtistFolder(artist)); - } - - _logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, source, destination); + _logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, sourcePath, destinationPath); - //TODO: Move to transactional disk operations try { - _diskTransferService.TransferFolder(source, destination, TransferMode.Move); + _diskTransferService.TransferFolder(sourcePath, destinationPath, TransferMode.Move); } catch (IOException ex) { - _logger.Error(ex, "Unable to move artist from '{0}' to '{1}'", source, destination); - throw; + _logger.Error(ex, "Unable to move artist from '{0}' to '{1}'. Try moving files manually", sourcePath, destinationPath); + + RevertPath(artist.Id, sourcePath); } _logger.ProgressInfo("{0} moved successfully to {1}", artist.Name, artist.Path); - //Update the artist path to the new path - artist.Path = destination; - artist = _artistService.UpdateArtist(artist); + _eventAggregator.PublishEvent(new ArtistMovedEvent(artist, sourcePath, destinationPath)); + } + + private void RevertPath(int artistId, string path) + { + var artist = _artistService.GetArtist(artistId); + + artist.Path = path; + _artistService.UpdateArtist(artist); + } + + public void Execute(MoveArtistCommand message) + { + var artist = _artistService.GetArtist(message.ArtistId); + MoveSingleArtist(artist, message.SourcePath, message.DestinationPath); + } + + public void Execute(BulkMoveArtistCommand message) + { + var artistToMove = message.Artist; + var destinationRootFolder = message.DestinationRootFolder; + + _logger.ProgressInfo("Moving {0} artist to '{1}'", artistToMove.Count, destinationRootFolder); + + foreach (var s in artistToMove) + { + var artist = _artistService.GetArtist(s.ArtistId); + var destinationPath = Path.Combine(destinationRootFolder, _filenameBuilder.GetArtistFolder(artist)); + + MoveSingleArtist(artist, s.SourcePath, destinationPath); + } - _eventAggregator.PublishEvent(new ArtistMovedEvent(artist, source, destination)); + _logger.ProgressInfo("Finished moving {0} artist to '{1}'", artistToMove.Count, destinationRootFolder); } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index d18316f1a..d71869ef8 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -758,6 +758,7 @@ +