UI Updates (Cancel Import, Move Artist, Manual Import from Artist)

Ability to cancel an import lookup/search at any point.
Ability to move artist path from Artist Edit or bulk move from Mass Editor.
Trigger manual import for Artist path from Artist Detail page.
Pulled from Sonarr
This commit is contained in:
Qstick 2017-12-29 22:23:04 -05:00
parent 5fae202760
commit d8c89f5bbd
79 changed files with 1075 additions and 376 deletions

View file

@ -58,7 +58,9 @@ class HistoryRow extends Component {
album, album,
track, track,
language, language,
languageCutoffNotMet,
quality, quality,
qualityCutoffNotMet,
eventType, eventType,
sourceTitle, sourceTitle,
date, date,
@ -135,6 +137,7 @@ class HistoryRow extends Component {
<TableRowCell key={name}> <TableRowCell key={name}>
<EpisodeLanguage <EpisodeLanguage
language={language} language={language}
isCutoffMet={languageCutoffNotMet}
/> />
</TableRowCell> </TableRowCell>
); );
@ -145,6 +148,7 @@ class HistoryRow extends Component {
<TableRowCell key={name}> <TableRowCell key={name}>
<EpisodeQuality <EpisodeQuality
quality={quality} quality={quality}
isCutoffMet={qualityCutoffNotMet}
/> />
</TableRowCell> </TableRowCell>
); );
@ -233,7 +237,9 @@ HistoryRow.propTypes = {
album: PropTypes.object, album: PropTypes.object,
track: PropTypes.object, track: PropTypes.object,
language: PropTypes.object.isRequired, language: PropTypes.object.isRequired,
languageCutoffNotMet: PropTypes.bool.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired, eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,

View file

@ -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;
}
}

View file

@ -19,6 +19,12 @@
height: 35px; height: 35px;
} }
.loadingButton {
composes: importButton;
margin-left: 10px;
}
.loading { .loading {
composes: loading from 'Components/Loading/LoadingIndicator.css'; composes: loading from 'Components/Loading/LoadingIndicator.css';

View file

@ -2,6 +2,7 @@ import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes, kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import CheckInput from 'Components/Form/CheckInput'; import CheckInput from 'Components/Form/CheckInput';
@ -117,7 +118,8 @@ class ImportArtistFooter extends Component {
isMetadataProfileIdMixed, isMetadataProfileIdMixed,
showLanguageProfile, showLanguageProfile,
showMetadataProfile, showMetadataProfile,
onImportPress onImportPress,
onCancelLookupPress
} = this.props; } = this.props;
const { const {
@ -227,10 +229,21 @@ class ImportArtistFooter extends Component {
{ {
isLookingUpArtist && isLookingUpArtist &&
<LoadingIndicator <Button
className={styles.loading} className={styles.loadingButton}
size={24} kind={kinds.WARNING}
/> onPress={onCancelLookupPress}
>
Cancel Processing
</Button>
}
{
isLookingUpArtist &&
<LoadingIndicator
className={styles.loading}
size={24}
/>
} }
{ {
@ -261,7 +274,8 @@ ImportArtistFooter.propTypes = {
showLanguageProfile: PropTypes.bool.isRequired, showLanguageProfile: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
onImportPress: PropTypes.func.isRequired onImportPress: PropTypes.func.isRequired,
onCancelLookupPress: PropTypes.func.isRequired
}; };
export default ImportArtistFooter; export default ImportArtistFooter;

View file

@ -2,6 +2,7 @@ import _ from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import ImportArtistFooter from './ImportArtistFooter'; import ImportArtistFooter from './ImportArtistFooter';
import { cancelLookupArtist } from 'Store/Actions/importArtistActions';
function isMixed(items, selectedIds, defaultValue, key) { function isMixed(items, selectedIds, defaultValue, key) {
return _.some(items, (artist) => { return _.some(items, (artist) => {
@ -23,11 +24,11 @@ function createMapStateToProps() {
albumFolder: defaultAlbumFolder albumFolder: defaultAlbumFolder
} = addArtist.defaults; } = addArtist.defaults;
const items = importArtist.items; const {
isLookingUpArtist,
const isLookingUpArtist = _.some(importArtist.items, (artist) => { isImporting,
return !artist.isPopulated && artist.error == null; items
}); } = importArtist;
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor'); const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId'); const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
@ -37,8 +38,8 @@ function createMapStateToProps() {
return { return {
selectedCount: selectedIds.length, selectedCount: selectedIds.length,
isImporting: importArtist.isImporting,
isLookingUpArtist, isLookingUpArtist,
isImporting,
defaultMonitor, defaultMonitor,
defaultQualityProfileId, defaultQualityProfileId,
defaultLanguageProfileId, defaultLanguageProfileId,
@ -54,4 +55,8 @@ function createMapStateToProps() {
); );
} }
export default connect(createMapStateToProps)(ImportArtistFooter); const mapDispatchToProps = {
onCancelLookupPress: cancelLookupArtist
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistFooter);

View file

@ -10,12 +10,6 @@ class ImportArtistTable extends Component {
// //
// Lifecycle // Lifecycle
constructor(props, context) {
super(props, context);
this._table = null;
}
componentDidMount() { componentDidMount() {
const { const {
unmappedFolders, unmappedFolders,
@ -101,22 +95,11 @@ class ImportArtistTable extends Component {
return; 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 // Control
setTableRef = (ref) => {
this._table = ref;
}
rowRenderer = ({ key, rowIndex, style }) => { rowRenderer = ({ key, rowIndex, style }) => {
const { const {
rootFolderId, rootFolderId,
@ -156,6 +139,7 @@ class ImportArtistTable extends Component {
showLanguageProfile, showLanguageProfile,
showMetadataProfile, showMetadataProfile,
scrollTop, scrollTop,
selectedState,
onSelectAllChange, onSelectAllChange,
onScroll onScroll
} = this.props; } = this.props;
@ -166,7 +150,6 @@ class ImportArtistTable extends Component {
return ( return (
<VirtualTable <VirtualTable
ref={this.setTableRef}
items={items} items={items}
contentBody={contentBody} contentBody={contentBody}
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
@ -183,6 +166,7 @@ class ImportArtistTable extends Component {
onSelectAllChange={onSelectAllChange} onSelectAllChange={onSelectAllChange}
/> />
} }
selectedState={selectedState}
onScroll={onScroll} onScroll={onScroll}
/> />
); );

View file

@ -66,6 +66,5 @@
.searchInput { .searchInput {
composes: text from 'Components/Form/TextInput.css'; composes: text from 'Components/Form/TextInput.css';
border-top-left-radius: 0; border-radius: 0;
border-bottom-left-radius: 0;
} }

View file

@ -5,6 +5,7 @@ import TetherComponent from 'react-tether';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import SpinnerIcon from 'Components/SpinnerIcon'; import SpinnerIcon from 'Components/SpinnerIcon';
import FormInputButton from 'Components/Form/FormInputButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
@ -99,6 +100,10 @@ class ImportArtistSelectArtist extends Component {
}); });
} }
onRefreshPress = () => {
this.props.onSearchInputChange(this.state.term);
}
onArtistSelect = (foreignArtistId) => { onArtistSelect = (foreignArtistId) => {
this.setState({ isOpen: false }); this.setState({ isOpen: false });
@ -116,7 +121,8 @@ class ImportArtistSelectArtist extends Component {
isPopulated, isPopulated,
error, error,
items, items,
queued queued,
isLookingUpArtist
} = this.props; } = this.props;
const errorMessage = error && const errorMessage = error &&
@ -137,7 +143,7 @@ class ImportArtistSelectArtist extends Component {
onPress={this.onPress} onPress={this.onPress}
> >
{ {
queued && !isPopulated && isLookingUpArtist && queued && !isPopulated &&
<LoadingIndicator <LoadingIndicator
className={styles.loading} className={styles.loading}
size={20} size={20}
@ -206,10 +212,7 @@ class ImportArtistSelectArtist extends Component {
<div className={styles.content}> <div className={styles.content}>
<div className={styles.searchContainer}> <div className={styles.searchContainer}>
<div className={styles.searchIconContainer}> <div className={styles.searchIconContainer}>
<SpinnerIcon <Icon name={icons.SEARCH} />
name={icons.SEARCH}
isSpinning={isFetching}
/>
</div> </div>
<TextInput <TextInput
@ -218,6 +221,16 @@ class ImportArtistSelectArtist extends Component {
value={this.state.term} value={this.state.term}
onChange={this.onSearchInputChange} onChange={this.onSearchInputChange}
/> />
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>
<Icon name={icons.REFRESH} />
</FormInputButton>
</div> </div>
<div className={styles.results}> <div className={styles.results}>
@ -253,6 +266,7 @@ ImportArtistSelectArtist.propTypes = {
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
queued: PropTypes.bool.isRequired, queued: PropTypes.bool.isRequired,
isLookingUpArtist: PropTypes.bool.isRequired,
onSearchInputChange: PropTypes.func.isRequired, onSearchInputChange: PropTypes.func.isRequired,
onArtistSelect: PropTypes.func.isRequired onArtistSelect: PropTypes.func.isRequired
}; };

View file

@ -9,9 +9,13 @@ import ImportArtistSelectArtist from './ImportArtistSelectArtist';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.importArtist.isLookingUpArtist,
createImportArtistItemSelector(), createImportArtistItemSelector(),
(item) => { (isLookingUpArtist, item) => {
return item; return {
isLookingUpArtist,
...item
};
} }
); );
} }
@ -29,7 +33,8 @@ class ImportArtistSelectArtistConnector extends Component {
onSearchInputChange = (term) => { onSearchInputChange = (term) => {
this.props.queueLookupArtist({ this.props.queueLookupArtist({
name: this.props.id, name: this.props.id,
term term,
topOfQueue: true
}); });
} }

View file

@ -1,11 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
function EpisodeLanguage(props) { function EpisodeLanguage(props) {
const { const {
className, className,
language language,
isCutoffNotMet
} = props; } = props;
if (!language) { if (!language) {
@ -13,7 +15,10 @@ function EpisodeLanguage(props) {
} }
return ( return (
<Label className={className}> <Label
className={className}
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
>
{language.name} {language.name}
</Label> </Label>
); );
@ -21,7 +26,12 @@ function EpisodeLanguage(props) {
EpisodeLanguage.propTypes = { EpisodeLanguage.propTypes = {
className: PropTypes.string, className: PropTypes.string,
language: PropTypes.object language: PropTypes.object,
isCutoffNotMet: PropTypes.bool
};
EpisodeLanguage.defaultProps = {
isCutoffNotMet: true
}; };
export default EpisodeLanguage; export default EpisodeLanguage;

View file

@ -100,7 +100,8 @@ EpisodeNumber.propTypes = {
}; };
EpisodeNumber.defaultProps = { EpisodeNumber.defaultProps = {
unverifiedSceneNumbering: false unverifiedSceneNumbering: false,
alternateTitles: []
}; };
export default EpisodeNumber; export default EpisodeNumber;

View file

@ -15,6 +15,11 @@ const columns = [
label: 'Source Title', label: 'Source Title',
isVisible: true isVisible: true
}, },
{
name: 'language',
label: 'Language',
isVisible: true
},
{ {
name: 'quality', name: 'quality',
label: 'Quality', label: 'Quality',

View file

@ -8,6 +8,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import EpisodeLanguage from 'Album/EpisodeLanguage';
import EpisodeQuality from 'Album/EpisodeQuality'; import EpisodeQuality from 'Album/EpisodeQuality';
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
@ -61,6 +62,8 @@ class AlbumHistoryRow extends Component {
const { const {
eventType, eventType,
sourceTitle, sourceTitle,
language,
languageCutoffNotMet,
quality, quality,
qualityCutoffNotMet, qualityCutoffNotMet,
date, date,
@ -83,6 +86,14 @@ class AlbumHistoryRow extends Component {
</TableRowCell> </TableRowCell>
<TableRowCell> <TableRowCell>
<EpisodeLanguage
language={language}
isCutoffNotMet={languageCutoffNotMet}
/>
</TableRowCell>
<TableRowCell>
<EpisodeQuality <EpisodeQuality
quality={quality} quality={quality}
isCutoffNotMet={qualityCutoffNotMet} isCutoffNotMet={qualityCutoffNotMet}
@ -140,6 +151,8 @@ AlbumHistoryRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
eventType: PropTypes.string.isRequired, eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
language: PropTypes.object.isRequired,
languageCutoffNotMet: PropTypes.bool.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,

View file

@ -1,39 +0,0 @@
@import "Content/icons";
.delete-artist-modal {
i {
margin-right : 5px;
//.fa-icon-color(white);
}
.path {
white-space : nowrap;
font-size : 16px;
padding-bottom : 20px;
}
.delete-files-info,
.delete-label {
color : @brand-danger-dark;
}
.delete-files-info {
display : none;
}
.checkbox {
display : inline-block;
}
.c-checkbox:hover .check {
border-color : @brand-danger-dark;
}
input[type=checkbox]:checked + span {
background-color : @brand-danger-dark;
border-color : @brand-danger-dark;
}
}

View file

@ -32,6 +32,7 @@ import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector';
import ArtistTagsConnector from './ArtistTagsConnector'; import ArtistTagsConnector from './ArtistTagsConnector';
import ArtistDetailsLinks from './ArtistDetailsLinks'; import ArtistDetailsLinks from './ArtistDetailsLinks';
import styles from './ArtistDetails.css'; import styles from './ArtistDetails.css';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
const albumTypes = [ const albumTypes = [
{ {
@ -94,6 +95,7 @@ class ArtistDetails extends Component {
isEditArtistModalOpen: false, isEditArtistModalOpen: false,
isDeleteArtistModalOpen: false, isDeleteArtistModalOpen: false,
isArtistHistoryModalOpen: false, isArtistHistoryModalOpen: false,
isInteractiveImportModalOpen: false,
allExpanded: false, allExpanded: false,
allCollapsed: false, allCollapsed: false,
expandedState: {} expandedState: {}
@ -119,6 +121,14 @@ class ArtistDetails extends Component {
this.setState({ isManageEpisodesOpen: false }); this.setState({ isManageEpisodesOpen: false });
} }
onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true });
}
onInteractiveImportModalClose = () => {
this.setState({ isInteractiveImportModalOpen: false });
}
onEditArtistPress = () => { onEditArtistPress = () => {
this.setState({ isEditArtistModalOpen: true }); this.setState({ isEditArtistModalOpen: true });
} }
@ -207,6 +217,7 @@ class ArtistDetails extends Component {
isEditArtistModalOpen, isEditArtistModalOpen,
isDeleteArtistModalOpen, isDeleteArtistModalOpen,
isArtistHistoryModalOpen, isArtistHistoryModalOpen,
isInteractiveImportModalOpen,
allExpanded, allExpanded,
allCollapsed, allCollapsed,
expandedState expandedState
@ -270,6 +281,12 @@ class ArtistDetails extends Component {
onPress={this.onArtistHistoryPress} onPress={this.onArtistHistoryPress}
/> />
<PageToolbarButton
label="Manual Import"
iconName={icons.INTERACTIVE}
onPress={this.onInteractiveImportPress}
/>
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
@ -574,6 +591,13 @@ class ArtistDetails extends Component {
artistId={id} artistId={id}
onModalClose={this.onDeleteArtistModalClose} onModalClose={this.onDeleteArtistModalClose}
/> />
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
folder={path}
showFilterExistingFiles={true}
onModalClose={this.onInteractiveImportModalClose}
/>
</PageContentBodyConnector> </PageContentBodyConnector>
</PageContent> </PageContent>
); );

View file

@ -11,10 +11,46 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import MoveArtistModal from 'Artist/MoveArtist/MoveArtistModal';
import styles from './EditArtistModalContent.css'; import styles from './EditArtistModalContent.css';
class EditArtistModalContent extends Component { class EditArtistModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isConfirmMoveModalOpen: false
};
}
//
// Listeners
onSavePress = () => {
const {
isPathChanging,
onSavePress
} = this.props;
if (isPathChanging && !this.state.isConfirmMoveModalOpen) {
this.setState({ isConfirmMoveModalOpen: true });
} else {
this.setState({ isConfirmMoveModalOpen: false });
onSavePress(false);
}
}
onMoveArtistPress = () => {
this.setState({ isConfirmMoveModalOpen: false });
this.props.onSavePress(true);
}
// //
// Render // Render
@ -25,8 +61,8 @@ class EditArtistModalContent extends Component {
isSaving, isSaving,
showLanguageProfile, showLanguageProfile,
showMetadataProfile, showMetadataProfile,
originalPath,
onInputChange, onInputChange,
onSavePress,
onModalClose, onModalClose,
onDeleteArtistPress, onDeleteArtistPress,
...otherProps ...otherProps
@ -156,11 +192,20 @@ class EditArtistModalContent extends Component {
<SpinnerButton <SpinnerButton
isSpinning={isSaving} isSpinning={isSaving}
onPress={onSavePress} onPress={this.onSavePress}
> >
Save Save
</SpinnerButton> </SpinnerButton>
</ModalFooter> </ModalFooter>
<MoveArtistModal
originalPath={originalPath}
destinationPath={path.value}
isOpen={this.state.isConfirmMoveModalOpen}
onSavePress={this.onSavePress}
onMoveArtistPress={this.onMoveArtistPress}
/>
</ModalContent> </ModalContent>
); );
} }
@ -173,6 +218,8 @@ EditArtistModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
showLanguageProfile: PropTypes.bool.isRequired, showLanguageProfile: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
isPathChanging: PropTypes.bool.isRequired,
originalPath: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,

View file

@ -37,7 +37,8 @@ function createMapStateToProps() {
artistName: artist.artistName, artistName: artist.artistName,
isSaving, isSaving,
saveError, saveError,
pendingChanges, isPathChanging: pendingChanges.hasOwnProperty('path'),
originalPath: artist.path,
item: settings.settings, item: settings.settings,
showLanguageProfile: languageProfiles.items.length > 1, showLanguageProfile: languageProfiles.items.length > 1,
showMetadataProfile: metadataProfiles.items.length > 1, showMetadataProfile: metadataProfiles.items.length > 1,
@ -48,8 +49,8 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
setArtistValue, dispatchSetArtistValue: setArtistValue,
saveArtist dispatchSaveArtist: saveArtist
}; };
class EditArtistModalContentConnector extends Component { class EditArtistModalContentConnector extends Component {
@ -67,11 +68,14 @@ class EditArtistModalContentConnector extends Component {
// Listeners // Listeners
onInputChange = ({ name, value }) => { onInputChange = ({ name, value }) => {
this.props.setArtistValue({ name, value }); this.props.dispatchSetArtistValue({ name, value });
} }
onSavePress = () => { onSavePress = (moveFiles) => {
this.props.saveArtist({ id: this.props.artistId }); this.props.dispatchSaveArtist({
id: this.props.artistId,
moveFiles
});
} }
// //
@ -83,6 +87,7 @@ class EditArtistModalContentConnector extends Component {
{...this.props} {...this.props}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onMoveArtistPress={this.onMoveArtistPress}
/> />
); );
} }
@ -92,8 +97,8 @@ EditArtistModalContentConnector.propTypes = {
artistId: PropTypes.number, artistId: PropTypes.number,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
setArtistValue: PropTypes.func.isRequired, dispatchSetArtistValue: PropTypes.func.isRequired,
saveArtist: PropTypes.func.isRequired, dispatchSaveArtist: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View file

@ -6,6 +6,7 @@ import createClientSideCollectionSelector from 'Store/Selectors/createClientSide
import createCommandSelector from 'Store/Selectors/createCommandSelector'; import createCommandSelector from 'Store/Selectors/createCommandSelector';
import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions'; import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import ArtistEditor from './ArtistEditor'; import ArtistEditor from './ArtistEditor';
@ -27,10 +28,11 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
setArtistEditorSort, dispatchSetArtistEditorSort: setArtistEditorSort,
setArtistEditorFilter, dispatchSetArtistEditorFilter: setArtistEditorFilter,
saveArtistEditor, dispatchSaveArtistEditor: saveArtistEditor,
fetchRootFolders dispatchFetchRootFolders: fetchRootFolders,
dispatchExecuteCommand: executeCommand
}; };
class ArtistEditorConnector extends Component { class ArtistEditorConnector extends Component {
@ -39,22 +41,29 @@ class ArtistEditorConnector extends Component {
// Lifecycle // Lifecycle
componentDidMount() { componentDidMount() {
this.props.fetchRootFolders(); this.props.dispatchFetchRootFolders();
} }
// //
// Listeners // Listeners
onSortPress = (sortKey) => { onSortPress = (sortKey) => {
this.props.setArtistEditorSort({ sortKey }); this.props.dispatchSetArtistEditorSort({ sortKey });
} }
onFilterSelect = (filterKey, filterValue, filterType) => { onFilterSelect = (filterKey, filterValue, filterType) => {
this.props.setArtistEditorFilter({ filterKey, filterValue, filterType }); this.props.dispatchSetArtistEditorFilter({ filterKey, filterValue, filterType });
} }
onSaveSelected = (payload) => { onSaveSelected = (payload) => {
this.props.saveArtistEditor(payload); this.props.dispatchSaveArtistEditor(payload);
}
onMoveSelected = (payload) => {
this.props.dispatchExecuteCommand({
name: commandNames.MOVE_ARTIST,
...payload
});
} }
// //
@ -73,10 +82,11 @@ class ArtistEditorConnector extends Component {
} }
ArtistEditorConnector.propTypes = { ArtistEditorConnector.propTypes = {
setArtistEditorSort: PropTypes.func.isRequired, dispatchSetArtistEditorSort: PropTypes.func.isRequired,
setArtistEditorFilter: PropTypes.func.isRequired, dispatchSetArtistEditorFilter: PropTypes.func.isRequired,
saveArtistEditor: PropTypes.func.isRequired, dispatchSaveArtistEditor: PropTypes.func.isRequired,
fetchRootFolders: PropTypes.func.isRequired dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
}; };
export default connectSection( export default connectSection(

View file

@ -8,6 +8,7 @@ import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSe
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector'; import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import MoveArtistModal from 'Artist/MoveArtist/MoveArtistModal';
import TagsModal from './Tags/TagsModal'; import TagsModal from './Tags/TagsModal';
import DeleteArtistModal from './Delete/DeleteArtistModal'; import DeleteArtistModal from './Delete/DeleteArtistModal';
import ArtistEditorFooterLabel from './ArtistEditorFooterLabel'; import ArtistEditorFooterLabel from './ArtistEditorFooterLabel';
@ -32,7 +33,9 @@ class ArtistEditorFooter extends Component {
rootFolderPath: NO_CHANGE, rootFolderPath: NO_CHANGE,
savingTags: false, savingTags: false,
isDeleteArtistModalOpen: false, isDeleteArtistModalOpen: false,
isTagsModalOpen: false isTagsModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
}; };
} }
@ -66,6 +69,12 @@ class ArtistEditorFooter extends Component {
} }
switch (name) { switch (name) {
case 'rootFolderPath':
this.setState({
isConfirmMoveModalOpen: true,
destinationRootFolder: value
});
break;
case 'monitored': case 'monitored':
this.props.onSaveSelected({ [name]: value === 'monitored' }); this.props.onSaveSelected({ [name]: value === 'monitored' });
break; break;
@ -105,6 +114,27 @@ class ArtistEditorFooter extends Component {
this.setState({ isTagsModalOpen: false }); this.setState({ isTagsModalOpen: false });
} }
onSaveRootFolderPress = () => {
this.setState({
isConfirmMoveModalOpen: false,
destinationRootFolder: null
});
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
}
onMoveArtistPress = () => {
this.setState({
isConfirmMoveModalOpen: false,
destinationRootFolder: null
});
this.props.onSaveSelected({
rootFolderPath: this.state.destinationRootFolder,
moveFiles: true
});
}
// //
// Render // Render
@ -129,7 +159,9 @@ class ArtistEditorFooter extends Component {
rootFolderPath, rootFolderPath,
savingTags, savingTags,
isTagsModalOpen, isTagsModalOpen,
isDeleteArtistModalOpen isDeleteArtistModalOpen,
isConfirmMoveModalOpen,
destinationRootFolder
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
@ -297,6 +329,14 @@ class ArtistEditorFooter extends Component {
artistIds={artistIds} artistIds={artistIds}
onModalClose={this.onDeleteArtistModalClose} onModalClose={this.onDeleteArtistModalClose}
/> />
<MoveArtistModal
destinationRootFolder={destinationRootFolder}
isOpen={isConfirmMoveModalOpen}
onSavePress={this.onSaveRootFolderPress}
onMoveArtistPress={this.onMoveArtistPress}
/>
</PageContentFooter> </PageContentFooter>
); );
} }

View file

@ -25,6 +25,11 @@ const columns = [
label: 'Source Title', label: 'Source Title',
isVisible: true isVisible: true
}, },
{
name: 'language',
label: 'Language',
isVisible: true
},
{ {
name: 'quality', name: 'quality',
label: 'Quality', label: 'Quality',

View file

@ -8,6 +8,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import EpisodeLanguage from 'Album/EpisodeLanguage';
import EpisodeQuality from 'Album/EpisodeQuality'; import EpisodeQuality from 'Album/EpisodeQuality';
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
@ -61,6 +62,8 @@ class ArtistHistoryRow extends Component {
const { const {
eventType, eventType,
sourceTitle, sourceTitle,
language,
languageCutoffNotMet,
quality, quality,
qualityCutoffNotMet, qualityCutoffNotMet,
date, date,
@ -89,6 +92,13 @@ class ArtistHistoryRow extends Component {
{sourceTitle} {sourceTitle}
</TableRowCell> </TableRowCell>
<TableRowCell>
<EpisodeLanguage
language={language}
isCutoffNotMet={languageCutoffNotMet}
/>
</TableRowCell>
<TableRowCell> <TableRowCell>
<EpisodeQuality <EpisodeQuality
quality={quality} quality={quality}
@ -147,6 +157,8 @@ ArtistHistoryRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
eventType: PropTypes.string.isRequired, eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
language: PropTypes.object.isRequired,
languageCutoffNotMet: PropTypes.bool.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,

View file

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector'; import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
import createLanguageProfileSelector from 'Store/Selectors/createLanguageProfileSelector'; import createLanguageProfileSelector from 'Store/Selectors/createLanguageProfileSelector';
@ -13,21 +14,21 @@ import * as commandNames from 'Commands/commandNames';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { id }) => id, createArtistSelector(),
(state, { albums }) => albums,
createQualityProfileSelector(), createQualityProfileSelector(),
createLanguageProfileSelector(), createLanguageProfileSelector(),
createMetadataProfileSelector(), createMetadataProfileSelector(),
createCommandsSelector(), createCommandsSelector(),
(artistId, albums, qualityProfile, languageProfile, metadataProfile, commands) => { (artist, qualityProfile, languageProfile, metadataProfile, commands) => {
const isRefreshingArtist = _.some(commands, (command) => { const isRefreshingArtist = _.some(commands, (command) => {
return command.name === commandNames.REFRESH_ARTIST && return command.name === commandNames.REFRESH_ARTIST &&
command.body.artistId === artistId; command.body.artistId === artist.id;
}); });
const latestAlbum = _.first(_.orderBy(albums, 'releaseDate', 'desc')); const latestAlbum = _.maxBy(artist.albums, (album) => album.releaseDate);
return { return {
...artist,
qualityProfile, qualityProfile,
languageProfile, languageProfile,
metadataProfile, metadataProfile,

View file

@ -31,8 +31,7 @@ $hoverScale: 1.05;
} }
.nextAiring { .nextAiring {
background-color: $defaultColor; background-color: #fafbfc;
color: $white;
text-align: center; text-align: center;
font-size: $smallFontSize; font-size: $smallFontSize;
} }

View file

@ -68,6 +68,7 @@ class ArtistIndexBanner extends Component {
bannerHeight, bannerHeight,
detailedProgressBar, detailedProgressBar,
showTitle, showTitle,
showMonitored,
showQualityProfile, showQualityProfile,
qualityProfile, qualityProfile,
showRelativeDates, showRelativeDates,
@ -151,6 +152,13 @@ class ArtistIndexBanner extends Component {
</div> </div>
} }
{
showMonitored &&
<div className={styles.title}>
{monitored ? 'Monitored' : 'Unmonitored'}
</div>
}
{ {
showQualityProfile && showQualityProfile &&
<div className={styles.title}> <div className={styles.title}>
@ -214,6 +222,7 @@ ArtistIndexBanner.propTypes = {
bannerHeight: PropTypes.number.isRequired, bannerHeight: PropTypes.number.isRequired,
detailedProgressBar: PropTypes.bool.isRequired, detailedProgressBar: PropTypes.bool.isRequired,
showTitle: PropTypes.bool.isRequired, showTitle: PropTypes.bool.isRequired,
showMonitored: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired,
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,

View file

@ -39,6 +39,7 @@ function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions)
const { const {
detailedProgressBar, detailedProgressBar,
showTitle, showTitle,
showMonitored,
showQualityProfile showQualityProfile
} = bannerOptions; } = bannerOptions;
@ -55,6 +56,10 @@ function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions)
heights.push(19); heights.push(19);
} }
if (showMonitored) {
heights.push(19);
}
if (showQualityProfile) { if (showQualityProfile) {
heights.push(19); heights.push(19);
} }
@ -213,6 +218,7 @@ class ArtistIndexBanners extends Component {
const { const {
detailedProgressBar, detailedProgressBar,
showTitle, showTitle,
showMonitored,
showQualityProfile showQualityProfile
} = bannerOptions; } = bannerOptions;
@ -231,12 +237,16 @@ class ArtistIndexBanners extends Component {
bannerHeight={bannerHeight} bannerHeight={bannerHeight}
detailedProgressBar={detailedProgressBar} detailedProgressBar={detailedProgressBar}
showTitle={showTitle} showTitle={showTitle}
showMonitored={showMonitored}
showQualityProfile={showQualityProfile} showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}
style={style} style={style}
{...artist} artistId={artist.id}
languageProfileId={artist.languageProfileId}
qualityProfileId={artist.qualityProfileId}
metadataProfileId={artist.metadataProfileId}
/> />
); );
} }

View file

@ -30,6 +30,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
detailedProgressBar: props.detailedProgressBar, detailedProgressBar: props.detailedProgressBar,
size: props.size, size: props.size,
showTitle: props.showTitle, showTitle: props.showTitle,
showMonitored: props.showMonitored,
showQualityProfile: props.showQualityProfile showQualityProfile: props.showQualityProfile
}; };
} }
@ -39,6 +40,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
detailedProgressBar, detailedProgressBar,
size, size,
showTitle, showTitle,
showMonitored,
showQualityProfile showQualityProfile
} = this.props; } = this.props;
@ -56,6 +58,10 @@ class ArtistIndexBannerOptionsModalContent extends Component {
state.showTitle = showTitle; state.showTitle = showTitle;
} }
if (showMonitored !== prevProps.showMonitored) {
state.showMonitored = showMonitored;
}
if (showQualityProfile !== prevProps.showQualityProfile) { if (showQualityProfile !== prevProps.showQualityProfile) {
state.showQualityProfile = showQualityProfile; state.showQualityProfile = showQualityProfile;
} }
@ -68,11 +74,11 @@ class ArtistIndexBannerOptionsModalContent extends Component {
// //
// Listeners // Listeners
onChangeOption = ({ name, value }) => { onChangeBannerOption = ({ name, value }) => {
this.setState({ this.setState({
[name]: value [name]: value
}, () => { }, () => {
this.props.onChangeOption({ [name]: value }); this.props.onChangeBannerOption({ [name]: value });
}); });
} }
@ -88,6 +94,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
detailedProgressBar, detailedProgressBar,
size, size,
showTitle, showTitle,
showMonitored,
showQualityProfile showQualityProfile
} = this.state; } = this.state;
@ -107,7 +114,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
name="size" name="size"
value={size} value={size}
values={bannerSizeOptions} values={bannerSizeOptions}
onChange={this.onChangeOption} onChange={this.onChangeBannerOption}
/> />
</FormGroup> </FormGroup>
@ -119,7 +126,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
name="detailedProgressBar" name="detailedProgressBar"
value={detailedProgressBar} value={detailedProgressBar}
helpText="Show text on progess bar" helpText="Show text on progess bar"
onChange={this.onChangeOption} onChange={this.onChangeBannerOption}
/> />
</FormGroup> </FormGroup>
@ -131,7 +138,19 @@ class ArtistIndexBannerOptionsModalContent extends Component {
name="showTitle" name="showTitle"
value={showTitle} value={showTitle}
helpText="Show artist name under banner" helpText="Show artist name under banner"
onChange={this.onChangeOption} onChange={this.onChangeBannerOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Show Monitored</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showMonitored"
value={showMonitored}
helpText="Show monitored status under banner"
onChange={this.onChangeBannerOption}
/> />
</FormGroup> </FormGroup>
@ -143,7 +162,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
name="showQualityProfile" name="showQualityProfile"
value={showQualityProfile} value={showQualityProfile}
helpText="Show quality profile under banner" helpText="Show quality profile under banner"
onChange={this.onChangeOption} onChange={this.onChangeBannerOption}
/> />
</FormGroup> </FormGroup>
</Form> </Form>
@ -166,7 +185,8 @@ ArtistIndexBannerOptionsModalContent.propTypes = {
showTitle: PropTypes.bool.isRequired, showTitle: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired,
detailedProgressBar: PropTypes.bool.isRequired, detailedProgressBar: PropTypes.bool.isRequired,
onChangeOption: PropTypes.func.isRequired, onChangeBannerOption: PropTypes.func.isRequired,
showMonitored: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View file

@ -14,7 +14,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onChangeOption(payload) { onChangeBannerOption(payload) {
dispatch(setArtistBannerOption(payload)); dispatch(setArtistBannerOption(payload));
} }
}; };

View file

@ -5,6 +5,7 @@ import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts'; import fonts from 'Styles/Variables/fonts';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ArtistPoster from 'Artist/ArtistPoster'; import ArtistPoster from 'Artist/ArtistPoster';
@ -188,6 +189,7 @@ class ArtistIndexOverview extends Component {
<ArtistIndexOverviewInfo <ArtistIndexOverviewInfo
height={overviewHeight} height={overviewHeight}
monitored={monitored}
nextAiring={nextAiring} nextAiring={nextAiring}
qualityProfile={qualityProfile} qualityProfile={qualityProfile}
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}

View file

@ -21,11 +21,13 @@ function isVisible(name, show, value, sortKey, index) {
function ArtistIndexOverviewInfo(props) { function ArtistIndexOverviewInfo(props) {
const { const {
height, height,
showMonitored,
showQualityProfile, showQualityProfile,
showAdded, showAdded,
showAlbumCount, showAlbumCount,
showPath, showPath,
showSizeOnDisk, showSizeOnDisk,
monitored,
nextAiring, nextAiring,
qualityProfile, qualityProfile,
added, added,
@ -47,6 +49,7 @@ function ArtistIndexOverviewInfo(props) {
} }
const maxRows = Math.floor(height / (infoRowHeight + 4)); const maxRows = Math.floor(height / (infoRowHeight + 4));
const monitoredText = monitored ? 'Monitored' : 'Unmonitored';
return ( return (
<div className={styles.infos}> <div className={styles.infos}>
@ -77,7 +80,23 @@ function ArtistIndexOverviewInfo(props) {
} }
{ {
isVisible('qualityProfileId', showQualityProfile, qualityProfile, sortKey) && maxRows > 1 && isVisible('monitored', showMonitored, monitored, sortKey) && maxRows > 1 &&
<div
className={styles.info}
title={monitoredText}
>
<Icon
className={styles.icon}
name={monitored ? icons.MONITORED : icons.UNMONITORED}
size={14}
/>
{monitoredText}
</div>
}
{
isVisible('qualityProfileId', showQualityProfile, qualityProfile, sortKey) && maxRows > 2 &&
<div <div
className={styles.info} className={styles.info}
title="Quality Profile" title="Quality Profile"
@ -93,7 +112,7 @@ function ArtistIndexOverviewInfo(props) {
} }
{ {
isVisible('added', showAdded, added, sortKey) && maxRows > 2 && isVisible('added', showAdded, added, sortKey) && maxRows > 3 &&
<div <div
className={styles.info} className={styles.info}
title="Date Added" title="Date Added"
@ -119,7 +138,7 @@ function ArtistIndexOverviewInfo(props) {
} }
{ {
isVisible('albumCount', showAlbumCount, albumCount, sortKey) && maxRows > 3 && isVisible('albumCount', showAlbumCount, albumCount, sortKey) && maxRows > 4 &&
<div <div
className={styles.info} className={styles.info}
title="Album Count" title="Album Count"
@ -135,7 +154,7 @@ function ArtistIndexOverviewInfo(props) {
} }
{ {
isVisible('path', showPath, path, sortKey) && maxRows > 4 && isVisible('path', showPath, path, sortKey) && maxRows > 5 &&
<div <div
className={styles.info} className={styles.info}
title="Path" title="Path"
@ -151,7 +170,7 @@ function ArtistIndexOverviewInfo(props) {
} }
{ {
isVisible('sizeOnDisk', showSizeOnDisk, sizeOnDisk, sortKey) && maxRows > 5 && isVisible('sizeOnDisk', showSizeOnDisk, sizeOnDisk, sortKey) && maxRows > 6 &&
<div <div
className={styles.info} className={styles.info}
title="Size on Disk" title="Size on Disk"
@ -173,11 +192,13 @@ function ArtistIndexOverviewInfo(props) {
ArtistIndexOverviewInfo.propTypes = { ArtistIndexOverviewInfo.propTypes = {
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
showNetwork: PropTypes.bool.isRequired, showNetwork: PropTypes.bool.isRequired,
showMonitored: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired,
showAdded: PropTypes.bool.isRequired, showAdded: PropTypes.bool.isRequired,
showAlbumCount: PropTypes.bool.isRequired, showAlbumCount: PropTypes.bool.isRequired,
showPath: PropTypes.bool.isRequired, showPath: PropTypes.bool.isRequired,
showSizeOnDisk: PropTypes.bool.isRequired, showSizeOnDisk: PropTypes.bool.isRequired,
monitored: PropTypes.bool.isRequired,
nextAiring: PropTypes.string, nextAiring: PropTypes.string,
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
previousAiring: PropTypes.string, previousAiring: PropTypes.string,

View file

@ -191,7 +191,10 @@ class ArtistIndexOverviews extends Component {
timeFormat={timeFormat} timeFormat={timeFormat}
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
style={style} style={style}
{...artist} artistId={artist.id}
languageProfileId={artist.languageProfileId}
qualityProfileId={artist.qualityProfileId}
metadataProfileId={artist.metadataProfileId}
/> />
); );
} }

View file

@ -29,6 +29,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
this.state = { this.state = {
detailedProgressBar: props.detailedProgressBar, detailedProgressBar: props.detailedProgressBar,
size: props.size, size: props.size,
showMonitored: props.showMonitored,
showQualityProfile: props.showQualityProfile, showQualityProfile: props.showQualityProfile,
showPreviousAiring: props.showPreviousAiring, showPreviousAiring: props.showPreviousAiring,
showAdded: props.showAdded, showAdded: props.showAdded,
@ -42,6 +43,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
const { const {
detailedProgressBar, detailedProgressBar,
size, size,
showMonitored,
showQualityProfile, showQualityProfile,
showPreviousAiring, showPreviousAiring,
showAdded, showAdded,
@ -60,6 +62,10 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
state.size = size; state.size = size;
} }
if (showMonitored !== prevProps.showMonitored) {
state.showMonitored = showMonitored;
}
if (showQualityProfile !== prevProps.showQualityProfile) { if (showQualityProfile !== prevProps.showQualityProfile) {
state.showQualityProfile = showQualityProfile; state.showQualityProfile = showQualityProfile;
} }
@ -111,6 +117,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
const { const {
detailedProgressBar, detailedProgressBar,
size, size,
showMonitored,
showQualityProfile, showQualityProfile,
showPreviousAiring, showPreviousAiring,
showAdded, showAdded,
@ -152,6 +159,18 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>Show Monitored</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showMonitored"
value={showMonitored}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Show Quality Profile</FormLabel> <FormLabel>Show Quality Profile</FormLabel>
<FormInputGroup <FormInputGroup
@ -233,6 +252,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
ArtistIndexOverviewOptionsModalContent.propTypes = { ArtistIndexOverviewOptionsModalContent.propTypes = {
size: PropTypes.string.isRequired, size: PropTypes.string.isRequired,
showMonitored: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired,
showPreviousAiring: PropTypes.bool.isRequired, showPreviousAiring: PropTypes.bool.isRequired,
showAdded: PropTypes.bool.isRequired, showAdded: PropTypes.bool.isRequired,

View file

@ -31,8 +31,7 @@ $hoverScale: 1.05;
} }
.nextAiring { .nextAiring {
background-color: $defaultColor; background-color: #fafbfc;
color: $white;
text-align: center; text-align: center;
font-size: $smallFontSize; font-size: $smallFontSize;
} }

View file

@ -68,6 +68,7 @@ class ArtistIndexPoster extends Component {
posterHeight, posterHeight,
detailedProgressBar, detailedProgressBar,
showTitle, showTitle,
showMonitored,
showQualityProfile, showQualityProfile,
qualityProfile, qualityProfile,
showRelativeDates, showRelativeDates,
@ -151,6 +152,13 @@ class ArtistIndexPoster extends Component {
</div> </div>
} }
{
showMonitored &&
<div className={styles.title}>
{monitored ? 'Monitored' : 'Unmonitored'}
</div>
}
{ {
showQualityProfile && showQualityProfile &&
<div className={styles.title}> <div className={styles.title}>
@ -214,6 +222,7 @@ ArtistIndexPoster.propTypes = {
posterHeight: PropTypes.number.isRequired, posterHeight: PropTypes.number.isRequired,
detailedProgressBar: PropTypes.bool.isRequired, detailedProgressBar: PropTypes.bool.isRequired,
showTitle: PropTypes.bool.isRequired, showTitle: PropTypes.bool.isRequired,
showMonitored: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired,
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,

View file

@ -39,6 +39,7 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
const { const {
detailedProgressBar, detailedProgressBar,
showTitle, showTitle,
showMonitored,
showQualityProfile showQualityProfile
} = posterOptions; } = posterOptions;
@ -55,6 +56,10 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
heights.push(19); heights.push(19);
} }
if (showMonitored) {
heights.push(19);
}
if (showQualityProfile) { if (showQualityProfile) {
heights.push(19); heights.push(19);
} }
@ -213,6 +218,7 @@ class ArtistIndexPosters extends Component {
const { const {
detailedProgressBar, detailedProgressBar,
showTitle, showTitle,
showMonitored,
showQualityProfile showQualityProfile
} = posterOptions; } = posterOptions;
@ -231,12 +237,16 @@ class ArtistIndexPosters extends Component {
posterHeight={posterHeight} posterHeight={posterHeight}
detailedProgressBar={detailedProgressBar} detailedProgressBar={detailedProgressBar}
showTitle={showTitle} showTitle={showTitle}
showMonitored={showMonitored}
showQualityProfile={showQualityProfile} showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}
style={style} style={style}
{...artist} artistId={artist.id}
languageProfileId={artist.languageProfileId}
qualityProfileId={artist.qualityProfileId}
metadataProfileId={artist.metadataProfileId}
/> />
); );
} }

View file

@ -30,6 +30,7 @@ class ArtistIndexPosterOptionsModalContent extends Component {
detailedProgressBar: props.detailedProgressBar, detailedProgressBar: props.detailedProgressBar,
size: props.size, size: props.size,
showTitle: props.showTitle, showTitle: props.showTitle,
showMonitored: props.showMonitored,
showQualityProfile: props.showQualityProfile showQualityProfile: props.showQualityProfile
}; };
} }
@ -39,6 +40,7 @@ class ArtistIndexPosterOptionsModalContent extends Component {
detailedProgressBar, detailedProgressBar,
size, size,
showTitle, showTitle,
showMonitored,
showQualityProfile showQualityProfile
} = this.props; } = this.props;
@ -56,6 +58,10 @@ class ArtistIndexPosterOptionsModalContent extends Component {
state.showTitle = showTitle; state.showTitle = showTitle;
} }
if (showMonitored !== prevProps.showMonitored) {
state.showMonitored = showMonitored;
}
if (showQualityProfile !== prevProps.showQualityProfile) { if (showQualityProfile !== prevProps.showQualityProfile) {
state.showQualityProfile = showQualityProfile; state.showQualityProfile = showQualityProfile;
} }
@ -88,6 +94,7 @@ class ArtistIndexPosterOptionsModalContent extends Component {
detailedProgressBar, detailedProgressBar,
size, size,
showTitle, showTitle,
showMonitored,
showQualityProfile showQualityProfile
} = this.state; } = this.state;
@ -135,6 +142,18 @@ class ArtistIndexPosterOptionsModalContent extends Component {
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>Show Monitored</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showMonitored"
value={showMonitored}
helpText="Show monitored status under poster"
onChange={this.onChangePosterOption}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel>Show Quality Profile</FormLabel> <FormLabel>Show Quality Profile</FormLabel>
@ -164,6 +183,7 @@ class ArtistIndexPosterOptionsModalContent extends Component {
ArtistIndexPosterOptionsModalContent.propTypes = { ArtistIndexPosterOptionsModalContent.propTypes = {
size: PropTypes.string.isRequired, size: PropTypes.string.isRequired,
showTitle: PropTypes.bool.isRequired, showTitle: PropTypes.bool.isRequired,
showMonitored: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired,
detailedProgressBar: PropTypes.bool.isRequired, detailedProgressBar: PropTypes.bool.isRequired,
onChangePosterOption: PropTypes.func.isRequired, onChangePosterOption: PropTypes.func.isRequired,

View file

@ -10,34 +10,6 @@ import styles from './ArtistIndexTable.css';
class ArtistIndexTable extends Component { 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 // Control
@ -59,10 +31,6 @@ class ArtistIndexTable extends Component {
} }
} }
setTableRef = (ref) => {
this._table = ref;
}
rowRenderer = ({ key, rowIndex, style }) => { rowRenderer = ({ key, rowIndex, style }) => {
const { const {
items, items,
@ -77,7 +45,10 @@ class ArtistIndexTable extends Component {
component={ArtistIndexRow} component={ArtistIndexRow}
style={style} style={style}
columns={columns} columns={columns}
{...artist} artistId={artist.id}
languageProfileId={artist.languageProfileId}
qualityProfileId={artist.qualityProfileId}
metadataProfileId={artist.metadataProfileId}
/> />
); );
} }
@ -89,6 +60,8 @@ class ArtistIndexTable extends Component {
const { const {
items, items,
columns, columns,
filterKey,
filterValue,
sortKey, sortKey,
sortDirection, sortDirection,
isSmallScreen, isSmallScreen,
@ -101,7 +74,6 @@ class ArtistIndexTable extends Component {
return ( return (
<VirtualTable <VirtualTable
ref={this.setTableRef}
className={styles.tableContainer} className={styles.tableContainer}
items={items} items={items}
scrollTop={scrollTop} scrollTop={scrollTop}
@ -118,6 +90,11 @@ class ArtistIndexTable extends Component {
onSortPress={onSortPress} onSortPress={onSortPress}
/> />
} }
columns={columns}
filterKey={filterKey}
filterValue={filterValue}
sortKey={sortKey}
sortDirection={sortDirection}
onRender={onRender} onRender={onRender}
onScroll={onScroll} onScroll={onScroll}
/> />

View file

@ -0,0 +1,5 @@
.doNotMoveButton {
composes: button from 'Components/Link/Button.css';
margin-right: auto;
}

View file

@ -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 (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
closeOnBackgroundClick={false}
onModalClose={onSavePress}
>
<ModalContent
showCloseButton={false}
onModalClose={onSavePress}
>
<ModalHeader>
Move Files
</ModalHeader>
<ModalBody>
{
destinationRootFolder ?
`Would you like to move the artist folders to ${destinationPath}'?` :
`Would you like to move the artist files from '${originalPath}' to '${destinationPath}'?`
}
</ModalBody>
<ModalFooter>
<Button
className={styles.doNotMoveButton}
onPress={onSavePress}
>
No, I'll Move the Files Myself
</Button>
<Button
kind={kinds.DANGER}
onPress={onMoveArtistPress}
>
Yes, Move the Files
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
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;

View file

@ -10,6 +10,7 @@ export const DOWNLOADED_ALBUMS_SCAN = 'DownloadedAlbumsScan';
export const ALBUM_SEARCH = 'AlbumSearch'; export const ALBUM_SEARCH = 'AlbumSearch';
export const INTERACTIVE_IMPORT = 'ManualImport'; export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_ALBUM_SEARCH = 'MissingAlbumSearch'; export const MISSING_ALBUM_SEARCH = 'MissingAlbumSearch';
export const MOVE_ARTIST = 'MoveArtist';
export const REFRESH_ARTIST = 'RefreshArtist'; export const REFRESH_ARTIST = 'RefreshArtist';
export const RENAME_FILES = 'RenameFiles'; export const RENAME_FILES = 'RenameFiles';
export const RENAME_ARTIST = 'RenameArtist'; export const RENAME_ARTIST = 'RenameArtist';

View file

@ -31,12 +31,19 @@
.isDisabled { .isDisabled {
opacity: 0.7; opacity: 0.7;
cursor: not-allowed; cursor: not-allowed;
pointer-events: all !important;
} }
.dropdownArrowContainer { .dropdownArrowContainer {
margin-left: 12px; margin-left: 12px;
} }
.dropdownArrowContainerDisabled {
composes: dropdownArrowContainer;
color: $disabledInputColor;
}
.optionsContainer { .optionsContainer {
width: auto; width: auto;
} }

View file

@ -289,6 +289,7 @@ class EnhancedSelectInput extends Component {
hasWarning && styles.hasWarning, hasWarning && styles.hasWarning,
isDisabled && disabledClassName isDisabled && disabledClassName
)} )}
isDisabled={isDisabled}
onBlur={this.onBlur} onBlur={this.onBlur}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onPress={this.onPress} onPress={this.onPress}
@ -296,11 +297,17 @@ class EnhancedSelectInput extends Component {
<SelectedValueComponent <SelectedValueComponent
{...selectedValueOptions} {...selectedValueOptions}
{...selectedOption} {...selectedOption}
isDisabled={isDisabled}
> >
{selectedOption ? selectedOption.value : null} {selectedOption ? selectedOption.value : null}
</SelectedValueComponent> </SelectedValueComponent>
<div className={styles.dropdownArrowContainer}> <div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
<Icon <Icon
name={icons.CARET_DOWN} name={icons.CARET_DOWN}
/> />

View file

@ -1,3 +1,7 @@
.selectedValue { .selectedValue {
flex: 1 1 auto; flex: 1 1 auto;
} }
.isDisabled {
color: $disabledInputColor;
}

View file

@ -1,15 +1,21 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import styles from './EnhancedSelectInputSelectedValue.css'; import styles from './EnhancedSelectInputSelectedValue.css';
function EnhancedSelectInputSelectedValue(props) { function EnhancedSelectInputSelectedValue(props) {
const { const {
className, className,
children children,
isDisabled
} = props; } = props;
return ( return (
<div className={className}> <div className={classNames(
className,
isDisabled && styles.isDisabled
)}
>
{children} {children}
</div> </div>
); );
@ -17,11 +23,13 @@ function EnhancedSelectInputSelectedValue(props) {
EnhancedSelectInputSelectedValue.propTypes = { EnhancedSelectInputSelectedValue.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
children: PropTypes.node children: PropTypes.node,
isDisabled: PropTypes.bool.isRequired
}; };
EnhancedSelectInputSelectedValue.defaultProps = { EnhancedSelectInputSelectedValue.defaultProps = {
className: styles.selectedValue className: styles.selectedValue,
isDisabled: false
}; };
export default EnhancedSelectInputSelectedValue; export default EnhancedSelectInputSelectedValue;

View file

@ -109,8 +109,17 @@ class Modal extends Component {
} }
onBackdropEndPress = (event) => { onBackdropEndPress = (event) => {
if (this._isBackdropPressed && this._isBackdropTarget(event)) { const {
this.props.onModalClose(); closeOnBackgroundClick,
onModalClose
} = this.props;
if (
this._isBackdropPressed &&
this._isBackdropTarget(event) &&
closeOnBackgroundClick
) {
onModalClose();
} }
this._isBackdropPressed = false; this._isBackdropPressed = false;
@ -187,13 +196,15 @@ Modal.propTypes = {
size: PropTypes.oneOf(sizes.all), size: PropTypes.oneOf(sizes.all),
children: PropTypes.node, children: PropTypes.node,
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
closeOnBackgroundClick: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
Modal.defaultProps = { Modal.defaultProps = {
className: styles.modal, className: styles.modal,
backdropClassName: styles.modalBackdrop, backdropClassName: styles.modalBackdrop,
size: sizes.LARGE size: sizes.LARGE,
closeOnBackgroundClick: true
}; };
export default Modal; export default Modal;

View file

@ -9,6 +9,7 @@ function ModalContent(props) {
const { const {
className, className,
children, children,
showCloseButton,
onModalClose, onModalClose,
...otherProps ...otherProps
} = props; } = props;
@ -18,15 +19,18 @@ function ModalContent(props) {
className={className} className={className}
{...otherProps} {...otherProps}
> >
<Link {
className={styles.closeButton} showCloseButton &&
onPress={onModalClose} <Link
> className={styles.closeButton}
<Icon onPress={onModalClose}
name={icons.CLOSE} >
size={18} <Icon
/> name={icons.CLOSE}
</Link> size={18}
/>
</Link>
}
{children} {children}
</div> </div>
@ -36,11 +40,13 @@ function ModalContent(props) {
ModalContent.propTypes = { ModalContent.propTypes = {
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
showCloseButton: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
ModalContent.defaultProps = { ModalContent.defaultProps = {
className: styles.modalContent className: styles.modalContent,
showCloseButton: true
}; };
export default ModalContent; export default ModalContent;

View file

@ -482,7 +482,7 @@ class PageSidebar extends Component {
key={child.to} key={child.to}
title={child.title} title={child.title}
to={child.to} to={child.to}
isActive={pathname === child.to} isActive={pathname.startsWith(child.to)}
isParentItem={false} isParentItem={false}
isChildItem={true} isChildItem={true}
statusComponent={child.statusComponent} statusComponent={child.statusComponent}

View file

@ -44,7 +44,6 @@ class VirtualTable extends Component {
}; };
this._isInitialized = false; this._isInitialized = false;
this._table = null;
} }
componentDidMount() { componentDidMount() {
@ -58,18 +57,9 @@ class VirtualTable extends Component {
return this.props.items[index]; return this.props.items[index];
} }
setTableRef = (ref) => {
this._table = ref;
}
forceUpdateGrid = () => {
this._table.recomputeGridSize();
}
scrollToRow = (rowIndex) => { scrollToRow = (rowIndex) => {
const scrollTop = (rowIndex + 1) * ROW_HEIGHT + 20; const scrollTop = (rowIndex + 1) * ROW_HEIGHT + 20;
// this._table.scrollToCell({ columnIndex: 0, rowIndex });
this.props.onScroll({ scrollTop }); this.props.onScroll({ scrollTop });
} }
@ -124,7 +114,6 @@ class VirtualTable extends Component {
{header} {header}
<VirtualTableBody <VirtualTableBody
ref={this.setTableRef}
autoContainerWidth={true} autoContainerWidth={true}
width={width} width={width}
height={height} height={height}

View file

@ -1,3 +1,14 @@
.filterContainer {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.filterText {
margin-left: 5px;
font-size: $largeFontSize;
}
.footer { .footer {
composes: modalFooter from 'Components/Modal/ModalFooter.css'; composes: modalFooter from 'Components/Modal/ModalFooter.css';

View file

@ -4,11 +4,15 @@ import React, { Component } from 'react';
import getSelectedIds from 'Utilities/Table/getSelectedIds'; import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll'; import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import { icons, kinds } from 'Helpers/Props'; import { align, icons, kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import SelectedMenuItem from 'Components/Menu/SelectedMenuItem';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
@ -70,6 +74,11 @@ const columns = [
} }
]; ];
const filterExistingFilesOptions = {
ALL: 'all',
NEW: 'new'
};
class InteractiveImportModalContent extends Component { class InteractiveImportModalContent extends Component {
// //
@ -129,6 +138,10 @@ class InteractiveImportModalContent extends Component {
this.props.onImportSelectedPress(selected, this.state.importMode); this.props.onImportSelectedPress(selected, this.state.importMode);
} }
onFilterExistingFilesChange = (value) => {
this.props.onFilterExistingFilesChange(value !== filterExistingFilesOptions.ALL);
}
onImportModeChange = ({ value }) => { onImportModeChange = ({ value }) => {
this.props.onImportModeChange(value); this.props.onImportModeChange(value);
} }
@ -155,6 +168,8 @@ class InteractiveImportModalContent extends Component {
render() { render() {
const { const {
downloadId, downloadId,
showFilterExistingFiles,
filterExistingFiles,
title, title,
folder, folder,
isFetching, isFetching,
@ -205,7 +220,45 @@ class InteractiveImportModalContent extends Component {
} }
{ {
isPopulated && !!items.length && isPopulated && showFilterExistingFiles && !isFetching &&
<div className={styles.filterContainer}>
<Menu alignMenu={align.RIGHT}>
<MenuButton>
<Icon
name={icons.FILTER}
size={22}
/>
<div className={styles.filterText}>
{
filterExistingFiles ? 'Unmapped Files Only' : 'All Files'
}
</div>
</MenuButton>
<MenuContent>
<SelectedMenuItem
name={filterExistingFilesOptions.ALL}
isSelected={!filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
All Files
</SelectedMenuItem>
<SelectedMenuItem
name={filterExistingFilesOptions.NEW}
isSelected={filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
Unmapped Files Only
</SelectedMenuItem>
</MenuContent>
</Menu>
</div>
}
{
isPopulated && !!items.length && !isFetching && !isFetching &&
<Table <Table
columns={columns} columns={columns}
selectAll={true} selectAll={true}
@ -235,7 +288,7 @@ class InteractiveImportModalContent extends Component {
} }
{ {
isPopulated && !items.length && isPopulated && !items.length && !isFetching &&
'No audio files were found in the selected folder' 'No audio files were found in the selected folder'
} }
</ModalBody> </ModalBody>
@ -303,6 +356,8 @@ class InteractiveImportModalContent extends Component {
InteractiveImportModalContent.propTypes = { InteractiveImportModalContent.propTypes = {
downloadId: PropTypes.string, downloadId: PropTypes.string,
showFilterExistingFiles: PropTypes.bool.isRequired,
filterExistingFiles: PropTypes.bool.isRequired,
importMode: PropTypes.string.isRequired, importMode: PropTypes.string.isRequired,
title: PropTypes.string, title: PropTypes.string,
folder: PropTypes.string, folder: PropTypes.string,
@ -314,12 +369,14 @@ InteractiveImportModalContent.propTypes = {
sortDirection: PropTypes.string, sortDirection: PropTypes.string,
interactiveImportErrorMessage: PropTypes.string, interactiveImportErrorMessage: PropTypes.string,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
onFilterExistingFilesChange: PropTypes.func.isRequired,
onImportModeChange: PropTypes.func.isRequired, onImportModeChange: PropTypes.func.isRequired,
onImportSelectedPress: PropTypes.func.isRequired, onImportSelectedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
InteractiveImportModalContent.defaultProps = { InteractiveImportModalContent.defaultProps = {
showFilterExistingFiles: false,
importMode: 'move' importMode: 'move'
}; };

View file

@ -35,7 +35,8 @@ class InteractiveImportModalContentConnector extends Component {
super(props, context); super(props, context);
this.state = { this.state = {
interactiveImportErrorMessage: null interactiveImportErrorMessage: null,
filterExistingFiles: true
}; };
} }
@ -45,7 +46,34 @@ class InteractiveImportModalContentConnector extends Component {
folder folder
} = this.props; } = 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() { componentWillUnmount() {
@ -59,6 +87,10 @@ class InteractiveImportModalContentConnector extends Component {
this.props.setInteractiveImportSort({ sortKey, sortDirection }); this.props.setInteractiveImportSort({ sortKey, sortDirection });
} }
onFilterExistingFilesChange = (filterExistingFiles) => {
this.setState({ filterExistingFiles });
}
onImportModeChange = (importMode) => { onImportModeChange = (importMode) => {
this.props.setInteractiveImportMode({ importMode }); this.props.setInteractiveImportMode({ importMode });
} }
@ -122,11 +154,18 @@ class InteractiveImportModalContentConnector extends Component {
// Render // Render
render() { render() {
const {
interactiveImportErrorMessage,
filterExistingFiles
} = this.state;
return ( return (
<InteractiveImportModalContent <InteractiveImportModalContent
{...this.props} {...this.props}
interactiveImportErrorMessage={this.state.interactiveImportErrorMessage} interactiveImportErrorMessage={interactiveImportErrorMessage}
filterExistingFiles={filterExistingFiles}
onSortPress={this.onSortPress} onSortPress={this.onSortPress}
onFilterExistingFilesChange={this.onFilterExistingFilesChange}
onImportModeChange={this.onImportModeChange} onImportModeChange={this.onImportModeChange}
onImportSelectedPress={this.onImportSelectedPress} onImportSelectedPress={this.onImportSelectedPress}
/> />
@ -137,6 +176,7 @@ class InteractiveImportModalContentConnector extends Component {
InteractiveImportModalContentConnector.propTypes = { InteractiveImportModalContentConnector.propTypes = {
downloadId: PropTypes.string, downloadId: PropTypes.string,
folder: PropTypes.string, folder: PropTypes.string,
filterExistingFiles: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchInteractiveImportItems: PropTypes.func.isRequired, fetchInteractiveImportItems: PropTypes.func.isRequired,
setInteractiveImportSort: PropTypes.func.isRequired, setInteractiveImportSort: PropTypes.func.isRequired,
@ -146,6 +186,10 @@ InteractiveImportModalContentConnector.propTypes = {
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
InteractiveImportModalContentConnector.defaultProps = {
filterExistingFiles: true
};
export default connectSection( export default connectSection(
createMapStateToProps, createMapStateToProps,
mapDispatchToProps, mapDispatchToProps,

View file

@ -15,6 +15,12 @@ import TableBody from 'Components/Table/TableBody';
import SelectTrackRow from './SelectTrackRow'; import SelectTrackRow from './SelectTrackRow';
const columns = [ const columns = [
{
name: 'mediumNumber',
label: 'Medium',
isSortable: true,
isVisible: true
},
{ {
name: 'trackNumber', name: 'trackNumber',
label: '#', label: '#',
@ -127,7 +133,8 @@ class SelectTrackModalContent extends Component {
<SelectTrackRow <SelectTrackRow
key={item.id} key={item.id}
id={item.id} id={item.id}
trackNumber={item.trackNumber} mediumNumber={item.mediumNumber}
trackNumber={item.absoluteTrackNumber}
title={item.title} title={item.title}
isSelected={selectedState[item.id]} isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange} onSelectedChange={this.onSelectedChange}

View file

@ -24,6 +24,7 @@ class SelectTrackRow extends Component {
render() { render() {
const { const {
id, id,
mediumNumber,
trackNumber, trackNumber,
title, title,
isSelected, isSelected,
@ -38,6 +39,10 @@ class SelectTrackRow extends Component {
onSelectedChange={onSelectedChange} onSelectedChange={onSelectedChange}
/> />
<TableRowCell>
{mediumNumber}
</TableRowCell>
<TableRowCell> <TableRowCell>
{trackNumber} {trackNumber}
</TableRowCell> </TableRowCell>
@ -53,6 +58,7 @@ class SelectTrackRow extends Component {
SelectTrackRow.propTypes = { SelectTrackRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
mediumNumber: PropTypes.number.isRequired,
trackNumber: PropTypes.number.isRequired, trackNumber: PropTypes.number.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,

View file

@ -264,7 +264,7 @@ class MediaManagement extends Component {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>File chmod mask</FormLabel> <FormLabel>File chmod mode</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
@ -279,7 +279,7 @@ class MediaManagement extends Component {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>Folder chmod mask</FormLabel> <FormLabel>Folder chmod mode</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}

View file

@ -6,13 +6,13 @@ function createRemoveItemHandler(section, url) {
return function(getState, payload, dispatch) { return function(getState, payload, dispatch) {
const { const {
id, id,
...queryParms ...queryParams
} = payload; } = payload;
dispatch(set({ section, isDeleting: true })); dispatch(set({ section, isDeleting: true }));
const ajaxOptions = { const ajaxOptions = {
url: `${url}/${id}?${$.param(queryParms, true)}`, url: `${url}/${id}?${$.param(queryParams, true)}`,
method: 'DELETE' method: 'DELETE'
}; };

View file

@ -1,3 +1,4 @@
import $ from 'jquery';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import getProviderState from 'Utilities/State/getProviderState'; import getProviderState from 'Utilities/State/getProviderState';
@ -14,15 +15,19 @@ export function createCancelSaveProviderHandler(section) {
}; };
} }
function createSaveProviderHandler(section, url) { function createSaveProviderHandler(section, url, options = {}) {
return function(getState, payload, dispatch) { return function(getState, payload, dispatch) {
dispatch(set({ section, isSaving: true })); dispatch(set({ section, isSaving: true }));
const id = payload.id; const {
id,
queryParams = {}
} = payload;
const saveData = getProviderState(payload, getState, section); const saveData = getProviderState(payload, getState, section);
const ajaxOptions = { const ajaxOptions = {
url, url: `${url}?${$.param(queryParams, true)}`,
method: 'POST', method: 'POST',
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
@ -30,7 +35,7 @@ function createSaveProviderHandler(section, url) {
}; };
if (id) { if (id) {
ajaxOptions.url = `${url}/${id}`; ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
ajaxOptions.method = 'PUT'; ajaxOptions.method = 'PUT';
} }

View file

@ -46,8 +46,31 @@ export const TOGGLE_ALBUM_MONITORED = 'artist/toggleAlbumMonitored';
// Action Creators // Action Creators
export const fetchArtist = createThunk(FETCH_ARTIST); export const fetchArtist = createThunk(FETCH_ARTIST);
export const saveArtist = createThunk(SAVE_ARTIST); export const saveArtist = createThunk(SAVE_ARTIST, (payload) => {
export const deleteArtist = createThunk(DELETE_ARTIST); 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 toggleArtistMonitored = createThunk(TOGGLE_ARTIST_MONITORED);
export const toggleAlbumMonitored = createThunk(TOGGLE_ALBUM_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 // Action Handlers
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[FETCH_ARTIST]: createFetchHandler(section, '/artist'), [FETCH_ARTIST]: createFetchHandler(section, '/artist'),
[SAVE_ARTIST]: createSaveProviderHandler(section, '/artist', { getAjaxOptions: getSaveAjaxOptions }),
[SAVE_ARTIST]: createSaveProviderHandler( [DELETE_ARTIST]: createRemoveItemHandler(section, '/artist'),
section, '/artist'),
[DELETE_ARTIST]: createRemoveItemHandler(
section,
'/artist'
),
[TOGGLE_ARTIST_MONITORED]: (getState, payload, dispatch) => { [TOGGLE_ARTIST_MONITORED]: (getState, payload, dispatch) => {
const { const {
@ -115,7 +143,7 @@ export const actionHandlers = handleThunks({
}); });
}, },
[TOGGLE_ALBUM_MONITORED]: (getState, payload, dispatch) => { [TOGGLE_ALBUM_MONITORED]: function(getState, payload, dispatch) {
const { const {
artistId: id, artistId: id,
seasonNumber, seasonNumber,

View file

@ -112,7 +112,7 @@ export const actionHandlers = handleThunks({
}); });
promise.done(() => { 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({ dispatch(set({
section, section,

View file

@ -28,6 +28,7 @@ export const defaultState = {
detailedProgressBar: false, detailedProgressBar: false,
size: 'large', size: 'large',
showTitle: false, showTitle: false,
showMonitored: true,
showQualityProfile: true showQualityProfile: true
}, },
@ -35,12 +36,14 @@ export const defaultState = {
detailedProgressBar: false, detailedProgressBar: false,
size: 'large', size: 'large',
showTitle: false, showTitle: false,
showMonitored: true,
showQualityProfile: true showQualityProfile: true
}, },
overviewOptions: { overviewOptions: {
detailedProgressBar: false, detailedProgressBar: false,
size: 'medium', size: 'medium',
showMonitored: true,
showNetwork: true, showNetwork: true,
showQualityProfile: true, showQualityProfile: true,
showPreviousAiring: false, showPreviousAiring: false,

View file

@ -2,6 +2,7 @@ import _ from 'lodash';
import $ from 'jquery'; import $ from 'jquery';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getSectionState from 'Utilities/State/getSectionState'; import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState'; import updateSectionState from 'Utilities/State/updateSectionState';
import getNewArtist from 'Utilities/Artist/getNewArtist'; import getNewArtist from 'Utilities/Artist/getNewArtist';
@ -15,14 +16,14 @@ import { fetchRootFolders } from './rootFolderActions';
export const section = 'importArtist'; export const section = 'importArtist';
let concurrentLookups = 0; let concurrentLookups = 0;
let abortCurrentLookup = null;
const queue = [];
// //
// State // State
export const defaultState = { export const defaultState = {
isFetching: false, isLookingUpArtist: false,
isPopulated: false,
error: null,
isImporting: false, isImporting: false,
isImported: false, isImported: false,
importError: null, importError: null,
@ -34,9 +35,10 @@ export const defaultState = {
export const QUEUE_LOOKUP_ARTIST = 'importArtist/queueLookupArtist'; export const QUEUE_LOOKUP_ARTIST = 'importArtist/queueLookupArtist';
export const START_LOOKUP_ARTIST = 'importArtist/startLookupArtist'; export const START_LOOKUP_ARTIST = 'importArtist/startLookupArtist';
export const CLEAR_IMPORT_ARTIST = 'importArtist/importArtist'; export const CANCEL_LOOKUP_ARTIST = 'importArtist/cancelLookupArtist';
export const SET_IMPORT_ARTIST_VALUE = 'importArtist/clearImportArtist'; export const CLEAR_IMPORT_ARTIST = 'importArtist/clearImportArtist';
export const IMPORT_ARTIST = 'importArtist/setImportArtistValue'; export const SET_IMPORT_ARTIST_VALUE = 'importArtist/setImportArtistValue';
export const IMPORT_ARTIST = 'importArtist/importArtist';
// //
// Action Creators // Action Creators
@ -45,10 +47,10 @@ export const queueLookupArtist = createThunk(QUEUE_LOOKUP_ARTIST);
export const startLookupArtist = createThunk(START_LOOKUP_ARTIST); export const startLookupArtist = createThunk(START_LOOKUP_ARTIST);
export const importArtist = createThunk(IMPORT_ARTIST); export const importArtist = createThunk(IMPORT_ARTIST);
export const clearImportArtist = createAction(CLEAR_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) => { export const setImportArtistValue = createAction(SET_IMPORT_ARTIST_VALUE, (payload) => {
return { return {
section, section,
...payload ...payload
}; };
@ -63,7 +65,8 @@ export const actionHandlers = handleThunks({
const { const {
name, name,
path, path,
term term,
topOfQueue = false
} = payload; } = payload;
const state = getState().importArtist; const state = getState().importArtist;
@ -84,8 +87,20 @@ export const actionHandlers = handleThunks({
items: [] 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) { if (term && term.length > 2) {
dispatch(startLookupArtist()); dispatch(startLookupArtist({ start: true }));
} }
}, },
@ -95,13 +110,27 @@ export const actionHandlers = handleThunks({
} }
const state = getState().importArtist; 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; return;
} }
concurrentLookups++; concurrentLookups++;
queue.splice(0, 1);
const queued = items.find((i) => i.id === queueId);
dispatch(updateItem({ dispatch(updateItem({
section, section,
@ -109,14 +138,16 @@ export const actionHandlers = handleThunks({
isFetching: true isFetching: true
})); }));
const promise = $.ajax({ const { request, abortRequest } = createAjaxRequest({
url: '/artist/lookup', url: '/artist/lookup',
data: { data: {
term: queued.term term: queued.term
} }
}); });
promise.done((data) => { abortCurrentLookup = abortRequest;
request.done((data) => {
dispatch(updateItem({ dispatch(updateItem({
section, section,
id: queued.id, id: queued.id,
@ -125,23 +156,26 @@ export const actionHandlers = handleThunks({
error: null, error: null,
items: data, items: data,
queued: false, queued: false,
selectedArtist: queued.selectedArtist || data[0] selectedArtist: queued.selectedArtist || data[0],
updateOnly: true
})); }));
}); });
promise.fail((xhr) => { request.fail((xhr) => {
dispatch(updateItem({ dispatch(updateItem({
section, section,
id: queued.id, id: queued.id,
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
error: xhr, error: xhr,
queued: false queued: false,
updateOnly: true
})); }));
}); });
promise.always(() => { request.always(() => {
concurrentLookups--; concurrentLookups--;
dispatch(startLookupArtist()); dispatch(startLookupArtist());
}); });
}, },
@ -159,7 +193,7 @@ export const actionHandlers = handleThunks({
// Make sure we have a selected artist and // Make sure we have a selected artist and
// the same artist hasn't been added yet. // 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); const newArtist = getNewArtist(_.cloneDeep(selectedArtist), item);
newArtist.path = item.path; newArtist.path = item.path;
@ -216,7 +250,19 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({ export const reducers = createHandleActions({
[CANCEL_LOOKUP_ARTIST]: function(state) {
return Object.assign({}, state, { isLookingUpArtist: false });
},
[CLEAR_IMPORT_ARTIST]: function(state) { [CLEAR_IMPORT_ARTIST]: function(state) {
if (abortCurrentLookup) {
abortCurrentLookup();
abortCurrentLookup = null;
}
queue.splice(0, queue.length);
return Object.assign({}, state, defaultState); return Object.assign({}, state, defaultState);
}, },

View file

@ -35,24 +35,22 @@ export const addTag = createThunk(ADD_TAG);
// Action Handlers // Action Handlers
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[FETCH_TAGS]: createFetchHandler('tags', '/tag'), [FETCH_TAGS]: createFetchHandler(section, '/tag'),
[ADD_TAG]: function(payload) { [ADD_TAG]: function(getState, payload, dispatch) {
return (dispatch, getState) => { const promise = $.ajax({
const promise = $.ajax({ url: '/tag',
url: '/tag', method: 'POST',
method: 'POST', data: JSON.stringify(payload.tag)
data: JSON.stringify(payload.tag) });
});
promise.done((data) => { promise.done((data) => {
const tags = getState().tags.items.slice(); const tags = getState().tags.items.slice();
tags.push(data); tags.push(data);
dispatch(update({ section: 'tags', data: tags })); dispatch(update({ section, data: tags }));
payload.onTagCreated(data); payload.onTagCreated(data);
}); });
};
} }
}); });

View file

@ -1,12 +1,16 @@
const thunks = {}; const thunks = {};
export function createThunk(type) { function identity(payload) {
return payload;
}
export function createThunk(type, identityFunction = identity) {
return function(payload = {}) { return function(payload = {}) {
return function(dispatch, getState) { return function(dispatch, getState) {
const thunk = thunks[type]; const thunk = thunks[type];
if (thunk) { if (thunk) {
return thunk(getState, payload, dispatch); return thunk(getState, identityFunction(payload), dispatch);
} }
throw Error(`Thunk handler has not been registered for ${type}`); throw Error(`Thunk handler has not been registered for ${type}`);
@ -21,4 +25,3 @@ export function handleThunks(handlers) {
thunks[type] = handlers[type]; thunks[type] = handlers[type];
}); });
} }

View file

@ -16,6 +16,7 @@ module.exports = {
sonarrBlue: '#00A65B', sonarrBlue: '#00A65B',
helpTextColor: '#909293', helpTextColor: '#909293',
gray: '#adadad', gray: '#adadad',
disabledInputColor: '#808080',
// Theme Colors // Theme Colors

View file

@ -1,7 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Nancy; using Nancy;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using Lidarr.Http.Extensions; using Lidarr.Http.Extensions;
namespace Lidarr.Api.V1.Artist namespace Lidarr.Api.V1.Artist
@ -9,11 +12,13 @@ namespace Lidarr.Api.V1.Artist
public class ArtistEditorModule : LidarrV1Module public class ArtistEditorModule : LidarrV1Module
{ {
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IManageCommandQueue _commandQueueManager;
public ArtistEditorModule(IArtistService artistService) public ArtistEditorModule(IArtistService artistService, IManageCommandQueue commandQueueManager)
: base("/artist/editor") : base("/artist/editor")
{ {
_artistService = artistService; _artistService = artistService;
_commandQueueManager = commandQueueManager;
Put["/"] = artist => SaveAll(); Put["/"] = artist => SaveAll();
Delete["/"] = artist => DeleteArtist(); Delete["/"] = artist => DeleteArtist();
} }
@ -22,6 +27,7 @@ namespace Lidarr.Api.V1.Artist
{ {
var resource = Request.Body.FromJson<ArtistEditorResource>(); var resource = Request.Body.FromJson<ArtistEditorResource>();
var artistToUpdate = _artistService.GetArtists(resource.ArtistIds); var artistToUpdate = _artistService.GetArtists(resource.ArtistIds);
var artistToMove = new List<BulkMoveArtist>();
foreach (var artist in artistToUpdate) foreach (var artist in artistToUpdate)
{ {
@ -53,6 +59,12 @@ namespace Lidarr.Api.V1.Artist
if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) if (resource.RootFolderPath.IsNotNullOrWhiteSpace())
{ {
artist.RootFolderPath = resource.RootFolderPath; artist.RootFolderPath = resource.RootFolderPath;
artistToMove.Add(new BulkMoveArtist
{
ArtistId = artist.Id,
SourcePath = artist.Path
});
} }
if (resource.Tags != null) 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) return _artistService.UpdateArtists(artistToUpdate)
.ToResource() .ToResource()
.AsResponse(HttpStatusCode.Accepted); .AsResponse(HttpStatusCode.Accepted);

View file

@ -14,6 +14,7 @@ namespace Lidarr.Api.V1.Artist
public string RootFolderPath { get; set; } public string RootFolderPath { get; set; }
public List<int> Tags { get; set; } public List<int> Tags { get; set; }
public ApplyTags ApplyTags { get; set; } public ApplyTags ApplyTags { get; set; }
public bool MoveFiles { get; set; }
} }
public enum ApplyTags public enum ApplyTags

View file

@ -7,9 +7,11 @@ using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ArtistStats; using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Music.Events; using NzbDrone.Core.Music.Events;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
@ -35,12 +37,14 @@ namespace Lidarr.Api.V1.Artist
private readonly IAddArtistService _addArtistService; private readonly IAddArtistService _addArtistService;
private readonly IArtistStatisticsService _artistStatisticsService; private readonly IArtistStatisticsService _artistStatisticsService;
private readonly IMapCoversToLocal _coverMapper; private readonly IMapCoversToLocal _coverMapper;
private readonly IManageCommandQueue _commandQueueManager;
public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster, public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster,
IArtistService artistService, IArtistService artistService,
IAddArtistService addArtistService, IAddArtistService addArtistService,
IArtistStatisticsService artistStatisticsService, IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper, IMapCoversToLocal coverMapper,
IManageCommandQueue commandQueueManager,
RootFolderValidator rootFolderValidator, RootFolderValidator rootFolderValidator,
ArtistPathValidator artistPathValidator, ArtistPathValidator artistPathValidator,
ArtistExistsValidator artistExistsValidator, ArtistExistsValidator artistExistsValidator,
@ -57,6 +61,7 @@ namespace Lidarr.Api.V1.Artist
_artistStatisticsService = artistStatisticsService; _artistStatisticsService = artistStatisticsService;
_coverMapper = coverMapper; _coverMapper = coverMapper;
_commandQueueManager = commandQueueManager;
GetResourceAll = AllArtists; GetResourceAll = AllArtists;
GetResourceById = GetArtist; GetResourceById = GetArtist;
@ -127,7 +132,24 @@ namespace Lidarr.Api.V1.Artist
private void UpdateArtist(ArtistResource artistResource) 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); _artistService.UpdateArtist(model);

View file

@ -55,10 +55,8 @@ namespace Lidarr.Api.V1.History
if (model.Artist != null) if (model.Artist != null)
{ {
resource.QualityCutoffNotMet = _upgradableSpecification.CutoffNotMet(model.Artist.Profile.Value, resource.QualityCutoffNotMet = _upgradableSpecification.QualityCutoffNotMet(model.Artist.Profile.Value, model.Quality);
model.Artist.LanguageProfile, resource.LanguageCutoffNotMet = _upgradableSpecification.LanguageCutoffNotMet(model.Artist.LanguageProfile, model.Language);
model.Quality,
model.Language);
} }
return resource; return resource;

View file

@ -19,6 +19,7 @@ namespace Lidarr.Api.V1.History
public Language Language { get; set; } public Language Language { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public bool QualityCutoffNotMet { get; set; } public bool QualityCutoffNotMet { get; set; }
public bool LanguageCutoffNotMet { get; set; }
public DateTime Date { get; set; } public DateTime Date { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }

View file

@ -3,6 +3,8 @@ using System.Linq;
using NzbDrone.Core.MediaFiles.TrackImport.Manual; using NzbDrone.Core.MediaFiles.TrackImport.Manual;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.Extensions;
namespace Lidarr.Api.V1.ManualImport namespace Lidarr.Api.V1.ManualImport
{ {
@ -22,8 +24,9 @@ namespace Lidarr.Api.V1.ManualImport
{ {
var folder = (string)Request.Query.folder; var folder = (string)Request.Query.folder;
var downloadId = (string)Request.Query.downloadId; 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) private ManualImportResource AddQualityWeight(ManualImportResource item)

View file

@ -22,6 +22,8 @@ namespace Lidarr.Api.V1.TrackFiles
public MediaInfoResource MediaInfo { get; set; } public MediaInfoResource MediaInfo { get; set; }
public bool QualityCutoffNotMet { get; set; } public bool QualityCutoffNotMet { get; set; }
public bool LanguageCutoffNotMet { get; set; }
} }
public static class TrackFileResourceMapper public static class TrackFileResourceMapper
@ -68,10 +70,8 @@ namespace Lidarr.Api.V1.TrackFiles
Quality = model.Quality, Quality = model.Quality,
MediaInfo = model.MediaInfo.ToResource(model.SceneName), MediaInfo = model.MediaInfo.ToResource(model.SceneName),
QualityCutoffNotMet = upgradableSpecification.CutoffNotMet(artist.Profile.Value, QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(artist.Profile.Value, model.Quality),
artist.LanguageProfile.Value, LanguageCutoffNotMet = upgradableSpecification.LanguageCutoffNotMet(artist.LanguageProfile.Value, model.Language)
model.Quality,
model.Language)
}; };
} }
} }

View file

@ -1,4 +1,6 @@
using System.IO; using System.Collections.Generic;
using System.IO;
using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
@ -16,6 +18,7 @@ namespace NzbDrone.Core.Test.MusicTests
{ {
private Artist _artist; private Artist _artist;
private MoveArtistCommand _command; private MoveArtistCommand _command;
private BulkMoveArtistCommand _bulkCommand;
[SetUp] [SetUp]
public void Setup() public void Setup()
@ -31,6 +34,19 @@ namespace NzbDrone.Core.Test.MusicTests
DestinationPath = @"C:\Test\Music2\Artist".AsOsAgnostic() DestinationPath = @"C:\Test\Music2\Artist".AsOsAgnostic()
}; };
_bulkCommand = new BulkMoveArtistCommand
{
Artist = new List<BulkMoveArtist>
{
new BulkMoveArtist
{
ArtistId = 1,
SourcePath = @"C:\Test\Music\Artist".AsOsAgnostic()
}
},
DestinationRootFolder = @"C:\Test\Music2".AsOsAgnostic()
};
Mocker.GetMock<IArtistService>() Mocker.GetMock<IArtistService>()
.Setup(s => s.GetArtist(It.IsAny<int>())) .Setup(s => s.GetArtist(It.IsAny<int>()))
.Returns(_artist); .Returns(_artist);
@ -48,52 +64,52 @@ namespace NzbDrone.Core.Test.MusicTests
{ {
GivenFailedMove(); GivenFailedMove();
Assert.Throws<IOException>(() => Subject.Execute(_command)); Subject.Execute(_command);
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
} }
[Test] [Test]
public void should_no_update_artist_path_on_error() public void should_revert_artist_path_on_error()
{ {
GivenFailedMove(); GivenFailedMove();
Assert.Throws<IOException>(() => Subject.Execute(_command)); Subject.Execute(_command);
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
Mocker.GetMock<IArtistService>() Mocker.GetMock<IArtistService>()
.Verify(v => v.UpdateArtist(It.IsAny<Artist>()), Times.Never()); .Verify(v => v.UpdateArtist(It.IsAny<Artist>()), Times.Once());
}
[Test]
public void should_use_destination_path()
{
Subject.Execute(_command);
Mocker.GetMock<IDiskTransferService>()
.Verify(v => v.TransferFolder(_command.SourcePath, _command.DestinationPath, TransferMode.Move, It.IsAny<bool>()), Times.Once());
Mocker.GetMock<IBuildFileNames>()
.Verify(v => v.GetArtistFolder(It.IsAny<Artist>(), null), Times.Never());
} }
[Test] [Test]
public void should_build_new_path_when_root_folder_is_provided() public void should_build_new_path_when_root_folder_is_provided()
{ {
_command.DestinationPath = null; var artistFolder = "Artist";
_command.DestinationRootFolder = @"C:\Test\Music3".AsOsAgnostic(); var expectedPath = Path.Combine(_bulkCommand.DestinationRootFolder, artistFolder);
var expectedPath = @"C:\Test\Music3\Artist".AsOsAgnostic();
Mocker.GetMock<IBuildFileNames>() Mocker.GetMock<IBuildFileNames>()
.Setup(s => s.GetArtistFolder(It.IsAny<Artist>(), null)) .Setup(s => s.GetArtistFolder(It.IsAny<Artist>(), null))
.Returns("Artist"); .Returns(artistFolder);
Subject.Execute(_command); Subject.Execute(_bulkCommand);
Mocker.GetMock<IArtistService>() Mocker.GetMock<IDiskTransferService>()
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Path == expectedPath)), Times.Once()); .Verify(v => v.TransferFolder(_bulkCommand.Artist.First().SourcePath, expectedPath, TransferMode.Move, It.IsAny<bool>()), Times.Once());
}
[Test]
public void should_use_destination_path_if_destination_root_folder_is_blank()
{
Subject.Execute(_command);
Mocker.GetMock<IArtistService>()
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Path == _command.DestinationPath)), Times.Once());
Mocker.GetMock<IBuildFileNames>()
.Verify(v => v.GetArtistFolder(It.IsAny<Artist>(), null), Times.Never());
} }
} }
} }

View file

@ -9,6 +9,8 @@ namespace NzbDrone.Core.DecisionEngine
public interface IUpgradableSpecification public interface IUpgradableSpecification
{ {
bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage); 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 CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null);
bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality); bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality);
} }
@ -68,29 +70,46 @@ namespace NzbDrone.Core.DecisionEngine
return true; 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); 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 (qualityCompare < 0)
if (languageCompare < 0)
{ {
return true; 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 true; 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;
}
if (QualityCutoffNotMet(profile, currentQuality, newQuality))
{
return true;
}
_logger.Debug("Existing item meets cut-off. skipping.");
return false;
} }
public bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality) public bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality)

View file

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.MediaFiles.Commands namespace NzbDrone.Core.MediaFiles.Commands
@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.Commands
public RenameArtistCommand() public RenameArtistCommand()
{ {
ArtistIds = new List<int>();
} }
} }
} }

View file

@ -19,6 +19,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
{ {
List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist); List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist);
List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo); List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo);
List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo, bool filterExistingFiles);
} }
public class ImportDecisionMaker : IMakeImportDecision public class ImportDecisionMaker : IMakeImportDecision
@ -52,14 +54,19 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
public List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo) public List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo)
{ {
var newFiles = _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist); return GetImportDecisions(musicFiles, artist, folderInfo, false);
}
_logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, musicFiles.Count()); public List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo, bool filterExistingFiles)
{
var files = filterExistingFiles ? _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist) : musicFiles.ToList();
_logger.Debug("Analyzing {0}/{1} files.", files.Count, musicFiles.Count);
var shouldUseFolderName = ShouldUseFolderName(musicFiles, artist, folderInfo); var shouldUseFolderName = ShouldUseFolderName(musicFiles, artist, folderInfo);
var decisions = new List<ImportDecision>(); var decisions = new List<ImportDecision>();
foreach (var file in newFiles) foreach (var file in files)
{ {
decisions.AddIfNotNull(GetDecision(file, artist, folderInfo, shouldUseFolderName)); decisions.AddIfNotNull(GetDecision(file, artist, folderInfo, shouldUseFolderName));
} }

View file

@ -20,7 +20,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
{ {
public interface IManualImportService public interface IManualImportService
{ {
List<ManualImportItem> GetMediaFiles(string path, string downloadId); List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles);
} }
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
@ -68,7 +68,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
_logger = logger; _logger = logger;
} }
public List<ManualImportItem> GetMediaFiles(string path, string downloadId) public List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles)
{ {
if (downloadId.IsNotNullOrWhiteSpace()) if (downloadId.IsNotNullOrWhiteSpace())
{ {
@ -92,10 +92,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
return new List<ManualImportItem> { ProcessFile(path, downloadId) }; return new List<ManualImportItem> { ProcessFile(path, downloadId) };
} }
return ProcessFolder(path, downloadId); return ProcessFolder(path, downloadId, filterExistingFiles);
} }
private List<ManualImportItem> ProcessFolder(string folder, string downloadId) private List<ManualImportItem> ProcessFolder(string folder, string downloadId, bool filterExistingFiles)
{ {
var directoryInfo = new DirectoryInfo(folder); var directoryInfo = new DirectoryInfo(folder);
var artist = _parsingService.GetArtist(directoryInfo.Name); var artist = _parsingService.GetArtist(directoryInfo.Name);
@ -115,7 +115,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
var folderInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name); var folderInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name);
var artistFiles = _diskScanService.GetAudioFiles(folder).ToList(); 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(); return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList();
} }

View file

@ -140,8 +140,11 @@ namespace NzbDrone.Core.Music
_logger.Trace("Updating: {0}", s.Name); _logger.Trace("Updating: {0}", s.Name);
if (!s.RootFolderPath.IsNullOrWhiteSpace()) if (!s.RootFolderPath.IsNullOrWhiteSpace())
{ {
var folderName = new DirectoryInfo(s.Path).Name; // Build the artist folder name instead of using the existing folder name.
s.Path = Path.Combine(s.RootFolderPath, folderName); // 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); _logger.Trace("Changing path for {0} to {1}", s.Name, s.Path);
} }

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Music.Commands
{
public class BulkMoveArtistCommand : Command
{
public List<BulkMoveArtist> 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; }
}
}

View file

@ -1,4 +1,4 @@
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Music.Commands namespace NzbDrone.Core.Music.Commands
{ {
@ -7,6 +7,7 @@ namespace NzbDrone.Core.Music.Commands
public int ArtistId { get; set; } public int ArtistId { get; set; }
public string SourcePath { get; set; } public string SourcePath { get; set; }
public string DestinationPath { get; set; } public string DestinationPath { get; set; }
public string DestinationRootFolder { get; set; }
public override bool SendUpdatesToClient => true;
} }
} }

View file

@ -1,7 +1,6 @@
using System.IO; using System.IO;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -11,7 +10,7 @@ using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.Music namespace NzbDrone.Core.Music
{ {
public class MoveArtistService : IExecute<MoveArtistCommand> public class MoveArtistService : IExecute<MoveArtistCommand>, IExecute<BulkMoveArtistCommand>
{ {
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IBuildFileNames _filenameBuilder; private readonly IBuildFileNames _filenameBuilder;
@ -32,38 +31,56 @@ namespace NzbDrone.Core.Music
_logger = logger; _logger = logger;
} }
public void Execute(MoveArtistCommand message) private void MoveSingleArtist(Artist artist, string sourcePath, string destinationPath)
{ {
var artist = _artistService.GetArtist(message.ArtistId); _logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, sourcePath, destinationPath);
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);
//TODO: Move to transactional disk operations
try try
{ {
_diskTransferService.TransferFolder(source, destination, TransferMode.Move); _diskTransferService.TransferFolder(sourcePath, destinationPath, TransferMode.Move);
} }
catch (IOException ex) catch (IOException ex)
{ {
_logger.Error(ex, "Unable to move artist from '{0}' to '{1}'", source, destination); _logger.Error(ex, "Unable to move artist from '{0}' to '{1}'. Try moving files manually", sourcePath, destinationPath);
throw;
RevertPath(artist.Id, sourcePath);
} }
_logger.ProgressInfo("{0} moved successfully to {1}", artist.Name, artist.Path); _logger.ProgressInfo("{0} moved successfully to {1}", artist.Name, artist.Path);
//Update the artist path to the new path _eventAggregator.PublishEvent(new ArtistMovedEvent(artist, sourcePath, destinationPath));
artist.Path = destination; }
artist = _artistService.UpdateArtist(artist);
_eventAggregator.PublishEvent(new ArtistMovedEvent(artist, source, destination)); 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);
}
_logger.ProgressInfo("Finished moving {0} artist to '{1}'", artistToMove.Count, destinationRootFolder);
} }
} }
} }

View file

@ -758,6 +758,7 @@
<Compile Include="Extras\Metadata\MetadataType.cs" /> <Compile Include="Extras\Metadata\MetadataType.cs" />
<Compile Include="Music\ArtistStatusType.cs" /> <Compile Include="Music\ArtistStatusType.cs" />
<Compile Include="Music\AlbumCutoffService.cs" /> <Compile Include="Music\AlbumCutoffService.cs" />
<Compile Include="Music\Commands\BulkMoveArtistCommand.cs" />
<Compile Include="Music\SecondaryAlbumType.cs" /> <Compile Include="Music\SecondaryAlbumType.cs" />
<Compile Include="Music\PrimaryAlbumType.cs" /> <Compile Include="Music\PrimaryAlbumType.cs" />
<Compile Include="Music\Links.cs" /> <Compile Include="Music\Links.cs" />