@@ -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 &&
+
@@ -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 &&
+
+
+
+ }
+
+ {
+ 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 @@
+