mirror of https://github.com/lidarr/Lidarr
New: Don't require artist mapping
This commit is contained in:
parent
34b1d429be
commit
a506125841
|
@ -2,7 +2,7 @@
|
|||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*.{cs}]
|
||||
[*.cs]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
|
|
@ -244,9 +244,7 @@ function HistoryDetails(props) {
|
|||
if (eventType === 'trackFileRenamed') {
|
||||
const {
|
||||
sourcePath,
|
||||
sourceRelativePath,
|
||||
path,
|
||||
relativePath
|
||||
path
|
||||
} = data;
|
||||
|
||||
return (
|
||||
|
@ -256,20 +254,10 @@ function HistoryDetails(props) {
|
|||
data={sourcePath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Source Relative Path"
|
||||
data={sourceRelativePath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Destination Path"
|
||||
data={path}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Destination Relative Path"
|
||||
data={relativePath}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
function ArtistMetadataProfilePopoverContent() {
|
||||
return (
|
||||
<div>
|
||||
Select 'None' to only include items manually added via search
|
||||
Select 'None' to only include items manually added via search or that match files on disk
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,166 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import ImportArtistTableConnector from './ImportArtistTableConnector';
|
||||
import ImportArtistFooterConnector from './ImportArtistFooterConnector';
|
||||
|
||||
class ImportArtist extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
scroller: null
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState, { parseIds: false });
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
// Only select non-dupes
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onRemoveSelectedStateItem = (id) => {
|
||||
this.setState((state) => {
|
||||
const selectedState = Object.assign({}, state.selectedState);
|
||||
delete selectedState[id];
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedState
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onInputChange(this.getSelectedIds(), name, value);
|
||||
}
|
||||
|
||||
onImportPress = () => {
|
||||
this.props.onImportPress(this.getSelectedIds());
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootFolderId,
|
||||
path,
|
||||
rootFoldersFetching,
|
||||
rootFoldersPopulated,
|
||||
rootFoldersError,
|
||||
unmappedFolders,
|
||||
showMetadataProfile
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
scroller
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title="Import Artist">
|
||||
<PageContentBodyConnector
|
||||
registerScroller={this.setScrollerRef}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{
|
||||
rootFoldersFetching && !rootFoldersPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersFetching && !!rootFoldersError &&
|
||||
<div>Unable to load root folders</div>
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !unmappedFolders.length &&
|
||||
<div>
|
||||
All artist in {path} have been imported
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
|
||||
<ImportArtistTableConnector
|
||||
rootFolderId={rootFolderId}
|
||||
unmappedFolders={unmappedFolders}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
selectedState={selectedState}
|
||||
scroller={scroller}
|
||||
showMetadataProfile={showMetadataProfile}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||
/>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
||||
<ImportArtistFooterConnector
|
||||
selectedIds={this.getSelectedIds()}
|
||||
showMetadataProfile={showMetadataProfile}
|
||||
onInputChange={this.onInputChange}
|
||||
onImportPress={this.onImportPress}
|
||||
/>
|
||||
}
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtist.propTypes = {
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
path: PropTypes.string,
|
||||
rootFoldersFetching: PropTypes.bool.isRequired,
|
||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||
rootFoldersError: PropTypes.object,
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportArtist.defaultProps = {
|
||||
unmappedFolders: []
|
||||
};
|
||||
|
||||
export default ImportArtist;
|
|
@ -1,170 +0,0 @@
|
|||
/* eslint max-params: 0 */
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setImportArtistValue, importArtist, clearImportArtist } from 'Store/Actions/importArtistActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { setAddDefault } from 'Store/Actions/searchActions';
|
||||
import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape';
|
||||
import ImportArtist from './ImportArtist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { match }) => match,
|
||||
(state) => state.rootFolders,
|
||||
(state) => state.addArtist,
|
||||
(state) => state.importArtist,
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
(
|
||||
match,
|
||||
rootFolders,
|
||||
addArtist,
|
||||
importArtistState,
|
||||
qualityProfiles,
|
||||
metadataProfiles
|
||||
) => {
|
||||
const {
|
||||
isFetching: rootFoldersFetching,
|
||||
isPopulated: rootFoldersPopulated,
|
||||
error: rootFoldersError,
|
||||
items
|
||||
} = rootFolders;
|
||||
|
||||
const rootFolderId = parseInt(match.params.rootFolderId);
|
||||
|
||||
const result = {
|
||||
rootFolderId,
|
||||
rootFoldersFetching,
|
||||
rootFoldersPopulated,
|
||||
rootFoldersError,
|
||||
qualityProfiles: qualityProfiles.items,
|
||||
metadataProfiles: metadataProfiles.items,
|
||||
showMetadataProfile: metadataProfiles.items.length > 1,
|
||||
defaultQualityProfileId: addArtist.defaults.qualityProfileId,
|
||||
defaultMetadataProfileId: addArtist.defaults.metadataProfileId
|
||||
};
|
||||
|
||||
if (items.length) {
|
||||
const rootFolder = _.find(items, { id: rootFolderId });
|
||||
|
||||
return {
|
||||
...result,
|
||||
...rootFolder,
|
||||
items: importArtistState.items
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetImportArtistValue: setImportArtistValue,
|
||||
dispatchImportArtist: importArtist,
|
||||
dispatchClearImportArtist: clearImportArtist,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchSetAddDefault: setAddDefault
|
||||
};
|
||||
|
||||
class ImportArtistConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
qualityProfiles,
|
||||
metadataProfiles,
|
||||
defaultQualityProfileId,
|
||||
defaultMetadataProfileId,
|
||||
dispatchFetchRootFolders,
|
||||
dispatchSetAddDefault
|
||||
} = this.props;
|
||||
|
||||
if (!this.props.rootFoldersPopulated) {
|
||||
dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
let setDefaults = false;
|
||||
const setDefaultPayload = {};
|
||||
|
||||
if (
|
||||
!defaultQualityProfileId ||
|
||||
!qualityProfiles.some((p) => p.id === defaultQualityProfileId)
|
||||
) {
|
||||
setDefaults = true;
|
||||
setDefaultPayload.qualityProfileId = qualityProfiles[0].id;
|
||||
}
|
||||
|
||||
if (
|
||||
!defaultMetadataProfileId ||
|
||||
!metadataProfiles.some((p) => p.id === defaultMetadataProfileId)
|
||||
) {
|
||||
setDefaults = true;
|
||||
setDefaultPayload.metadataProfileId = metadataProfiles[0].id;
|
||||
}
|
||||
|
||||
if (setDefaults) {
|
||||
dispatchSetAddDefault(setDefaultPayload);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearImportArtist();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = (ids, name, value) => {
|
||||
this.props.dispatchSetAddDefault({ [name]: value });
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.dispatchSetImportArtistValue({
|
||||
id,
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onImportPress = (ids) => {
|
||||
this.props.dispatchImportArtist({ ids });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportArtist
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onImportPress={this.onImportPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const routeMatchShape = createRouteMatchShape({
|
||||
rootFolderId: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
ImportArtistConnector.propTypes = {
|
||||
match: routeMatchShape.isRequired,
|
||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
metadataProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
defaultQualityProfileId: PropTypes.number.isRequired,
|
||||
defaultMetadataProfileId: PropTypes.number.isRequired,
|
||||
dispatchSetImportArtistValue: PropTypes.func.isRequired,
|
||||
dispatchImportArtist: PropTypes.func.isRequired,
|
||||
dispatchClearImportArtist: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchSetAddDefault: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistConnector);
|
|
@ -1,33 +0,0 @@
|
|||
.inputContainer {
|
||||
margin-right: 20px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.importButtonContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.importButton {
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.loadingButton {
|
||||
composes: importButton;
|
||||
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
composes: loading from '~Components/Loading/LoadingIndicator.css';
|
||||
|
||||
margin: 0 10px 0 12px;
|
||||
text-align: left;
|
||||
}
|
|
@ -1,261 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import styles from './ImportArtistFooter.css';
|
||||
|
||||
const MIXED = 'mixed';
|
||||
|
||||
class ImportArtistFooter extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultMetadataProfileId,
|
||||
defaultAlbumFolder
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
metadataProfileId: defaultMetadataProfileId,
|
||||
albumFolder: defaultAlbumFolder
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultMetadataProfileId,
|
||||
defaultAlbumFolder,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
isMetadataProfileIdMixed,
|
||||
isAlbumFolderMixed
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
albumFolder
|
||||
} = this.state;
|
||||
|
||||
const newState = {};
|
||||
|
||||
if (isMonitorMixed && monitor !== MIXED) {
|
||||
newState.monitor = MIXED;
|
||||
} else if (!isMonitorMixed && monitor !== defaultMonitor) {
|
||||
newState.monitor = defaultMonitor;
|
||||
}
|
||||
|
||||
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
|
||||
newState.qualityProfileId = MIXED;
|
||||
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
|
||||
newState.qualityProfileId = defaultQualityProfileId;
|
||||
}
|
||||
|
||||
if (isMetadataProfileIdMixed && metadataProfileId !== MIXED) {
|
||||
newState.metadataProfileId = MIXED;
|
||||
} else if (!isMetadataProfileIdMixed && metadataProfileId !== defaultMetadataProfileId) {
|
||||
newState.metadataProfileId = defaultMetadataProfileId;
|
||||
}
|
||||
|
||||
if (isAlbumFolderMixed && albumFolder != null) {
|
||||
newState.albumFolder = null;
|
||||
} else if (!isAlbumFolderMixed && albumFolder !== defaultAlbumFolder) {
|
||||
newState.albumFolder = defaultAlbumFolder;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(newState)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
this.props.onInputChange({ name, value });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedCount,
|
||||
isImporting,
|
||||
isLookingUpArtist,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
isMetadataProfileIdMixed,
|
||||
hasUnsearchedItems,
|
||||
showMetadataProfile,
|
||||
onImportPress,
|
||||
onLookupPress,
|
||||
onCancelLookupPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
albumFolder
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Monitor
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_ALBUMS_SELECT}
|
||||
name="monitor"
|
||||
value={monitor}
|
||||
isDisabled={!selectedCount}
|
||||
includeMixed={isMonitorMixed}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Quality Profile
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
isDisabled={!selectedCount}
|
||||
includeMixed={isQualityProfileIdMixed}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
showMetadataProfile &&
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Metadata Profile
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.METADATA_PROFILE_SELECT}
|
||||
name="metadataProfileId"
|
||||
value={metadataProfileId}
|
||||
isDisabled={!selectedCount}
|
||||
includeMixed={isMetadataProfileIdMixed}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Album Folder
|
||||
</div>
|
||||
|
||||
<CheckInput
|
||||
name="albumFolder"
|
||||
value={albumFolder}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles.label}>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.importButtonContainer}>
|
||||
<SpinnerButton
|
||||
className={styles.importButton}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isImporting}
|
||||
isDisabled={!selectedCount || isLookingUpArtist}
|
||||
onPress={onImportPress}
|
||||
>
|
||||
Import {selectedCount} Artist(s)
|
||||
</SpinnerButton>
|
||||
|
||||
{
|
||||
isLookingUpArtist &&
|
||||
<Button
|
||||
className={styles.loadingButton}
|
||||
kind={kinds.WARNING}
|
||||
onPress={onCancelLookupPress}
|
||||
>
|
||||
Cancel Processing
|
||||
</Button>
|
||||
}
|
||||
|
||||
{
|
||||
hasUnsearchedItems &&
|
||||
<Button
|
||||
className={styles.loadingButton}
|
||||
kind={kinds.SUCCESS}
|
||||
onPress={onLookupPress}
|
||||
>
|
||||
Start Processing
|
||||
</Button>
|
||||
}
|
||||
|
||||
{
|
||||
isLookingUpArtist &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={24}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isLookingUpArtist &&
|
||||
'Processing Folders'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PageContentFooter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtistFooter.propTypes = {
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
isImporting: PropTypes.bool.isRequired,
|
||||
isLookingUpArtist: PropTypes.bool.isRequired,
|
||||
defaultMonitor: PropTypes.string.isRequired,
|
||||
defaultQualityProfileId: PropTypes.number,
|
||||
defaultMetadataProfileId: PropTypes.number,
|
||||
defaultAlbumFolder: PropTypes.bool.isRequired,
|
||||
isMonitorMixed: PropTypes.bool.isRequired,
|
||||
isQualityProfileIdMixed: PropTypes.bool.isRequired,
|
||||
isMetadataProfileIdMixed: PropTypes.bool.isRequired,
|
||||
isAlbumFolderMixed: PropTypes.bool.isRequired,
|
||||
hasUnsearchedItems: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired,
|
||||
onLookupPress: PropTypes.func.isRequired,
|
||||
onCancelLookupPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportArtistFooter;
|
|
@ -1,61 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import ImportArtistFooter from './ImportArtistFooter';
|
||||
import { lookupUnsearchedArtist, cancelLookupArtist } from 'Store/Actions/importArtistActions';
|
||||
|
||||
function isMixed(items, selectedIds, defaultValue, key) {
|
||||
return _.some(items, (artist) => {
|
||||
return selectedIds.indexOf(artist.id) > -1 && artist[key] !== defaultValue;
|
||||
});
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addArtist,
|
||||
(state) => state.importArtist,
|
||||
(state, { selectedIds }) => selectedIds,
|
||||
(addArtist, importArtist, selectedIds) => {
|
||||
const {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
metadataProfileId: defaultMetadataProfileId,
|
||||
albumFolder: defaultAlbumFolder
|
||||
} = addArtist.defaults;
|
||||
|
||||
const {
|
||||
isLookingUpArtist,
|
||||
isImporting,
|
||||
items
|
||||
} = importArtist;
|
||||
|
||||
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
||||
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
|
||||
const isMetadataProfileIdMixed = isMixed(items, selectedIds, defaultMetadataProfileId, 'metadataProfileId');
|
||||
const isAlbumFolderMixed = isMixed(items, selectedIds, defaultAlbumFolder, 'albumFolder');
|
||||
const hasUnsearchedItems = !isLookingUpArtist && items.some((item) => !item.isPopulated);
|
||||
|
||||
return {
|
||||
selectedCount: selectedIds.length,
|
||||
isLookingUpArtist,
|
||||
isImporting,
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultMetadataProfileId,
|
||||
defaultAlbumFolder,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
isMetadataProfileIdMixed,
|
||||
isAlbumFolderMixed,
|
||||
hasUnsearchedItems
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onLookupPress: lookupUnsearchedArtist,
|
||||
onCancelLookupPress: cancelLookupArtist
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistFooter);
|
|
@ -1,38 +0,0 @@
|
|||
.folder {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 200px;
|
||||
}
|
||||
|
||||
.monitor {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 185px;
|
||||
}
|
||||
|
||||
.qualityProfile,
|
||||
.metadataProfile {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 250px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.albumFolder {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 150px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.artist {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 400px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.detailsIcon {
|
||||
margin-left: 8px;
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent';
|
||||
// import SeriesTypePopoverContent from 'AddArtist/SeriesTypePopoverContent';
|
||||
import styles from './ImportArtistHeader.css';
|
||||
|
||||
function ImportArtistHeader(props) {
|
||||
const {
|
||||
showMetadataProfile,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableHeader>
|
||||
<VirtualTableSelectAllHeaderCell
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.folder}
|
||||
name="folder"
|
||||
>
|
||||
Folder
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.monitor}
|
||||
name="monitor"
|
||||
>
|
||||
Monitor
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.detailsIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title="Monitoring Options"
|
||||
body={<ArtistMonitoringOptionsPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.qualityProfile}
|
||||
name="qualityProfileId"
|
||||
>
|
||||
Quality Profile
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
{
|
||||
showMetadataProfile &&
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.metadataProfile}
|
||||
name="metadataProfileId"
|
||||
>
|
||||
Metadata Profile
|
||||
</VirtualTableHeaderCell>
|
||||
}
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.albumFolder}
|
||||
name="albumFolder"
|
||||
>
|
||||
Album Folder
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.artist}
|
||||
name="artist"
|
||||
>
|
||||
Artist
|
||||
</VirtualTableHeaderCell>
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
}
|
||||
|
||||
ImportArtistHeader.propTypes = {
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportArtistHeader;
|
|
@ -1,45 +0,0 @@
|
|||
.selectInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
}
|
||||
|
||||
.folder {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 1 0 200px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.monitor {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 185px;
|
||||
}
|
||||
|
||||
.qualityProfile,
|
||||
.metadataProfile {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 250px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.albumFolder {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 150px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.artist {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 400px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.hideMetadataProfile {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
display: none;
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import ImportArtistSelectArtistConnector from './SelectArtist/ImportArtistSelectArtistConnector';
|
||||
import styles from './ImportArtistRow.css';
|
||||
|
||||
function ImportArtistRow(props) {
|
||||
const {
|
||||
id,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
albumFolder,
|
||||
selectedArtist,
|
||||
isExistingArtist,
|
||||
showMetadataProfile,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.selectInput}
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
isDisabled={!selectedArtist || isExistingArtist}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<VirtualTableRowCell className={styles.folder}>
|
||||
{id}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.monitor}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_ALBUMS_SELECT}
|
||||
name="monitor"
|
||||
value={monitor}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.qualityProfile}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell
|
||||
className={showMetadataProfile ? styles.metadataProfile : styles.hideMetadataProfile}
|
||||
>
|
||||
<FormInputGroup
|
||||
type={inputTypes.METADATA_PROFILE_SELECT}
|
||||
name="metadataProfileId"
|
||||
value={metadataProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.albumFolder}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="albumFolder"
|
||||
value={albumFolder}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.artist}>
|
||||
<ImportArtistSelectArtistConnector
|
||||
id={id}
|
||||
isExistingArtist={isExistingArtist}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ImportArtistRow.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
metadataProfileId: PropTypes.number.isRequired,
|
||||
albumFolder: PropTypes.bool.isRequired,
|
||||
selectedArtist: PropTypes.object,
|
||||
isExistingArtist: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportArtistRow.defaultsProps = {
|
||||
items: []
|
||||
};
|
||||
|
||||
export default ImportArtistRow;
|
|
@ -1,87 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setImportArtistValue } from 'Store/Actions/importArtistActions';
|
||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||
import ImportArtistRow from './ImportArtistRow';
|
||||
|
||||
function createImportArtistItemSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.importArtist.items,
|
||||
(id, items) => {
|
||||
return _.find(items, { id }) || {};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createImportArtistItemSelector(),
|
||||
createAllArtistSelector(),
|
||||
(item, artist) => {
|
||||
const selectedArtist = item && item.selectedArtist;
|
||||
const isExistingArtist = !!selectedArtist && _.some(artist, { foreignArtistId: selectedArtist.foreignArtistId });
|
||||
|
||||
return {
|
||||
...item,
|
||||
isExistingArtist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setImportArtistValue
|
||||
};
|
||||
|
||||
class ImportArtistRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setImportArtistValue({
|
||||
id: this.props.id,
|
||||
[name]: value
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
// Don't show the row until we have the information we require for it.
|
||||
|
||||
const {
|
||||
items,
|
||||
monitor,
|
||||
albumFolder
|
||||
} = this.props;
|
||||
|
||||
if (!items || !monitor || !albumFolder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImportArtistRow
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onArtistSelect={this.onArtistSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtistRowConnector.propTypes = {
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string,
|
||||
albumFolder: PropTypes.bool,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
setImportArtistValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistRowConnector);
|
|
@ -1,3 +0,0 @@
|
|||
.input {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import ImportArtistHeader from './ImportArtistHeader';
|
||||
import ImportArtistRowConnector from './ImportArtistRowConnector';
|
||||
|
||||
class ImportArtistTable extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
unmappedFolders,
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultMetadataProfileId,
|
||||
defaultAlbumFolder,
|
||||
onArtistLookup,
|
||||
onSetImportArtistValue
|
||||
} = this.props;
|
||||
|
||||
const values = {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
metadataProfileId: defaultMetadataProfileId,
|
||||
albumFolder: defaultAlbumFolder
|
||||
};
|
||||
|
||||
unmappedFolders.forEach((unmappedFolder) => {
|
||||
const id = unmappedFolder.name;
|
||||
|
||||
onArtistLookup(id, unmappedFolder.path);
|
||||
|
||||
onSetImportArtistValue({
|
||||
id,
|
||||
...values
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// This isn't great, but it's the most reliable way to ensure the items
|
||||
// are checked off even if they aren't actually visible since the cells
|
||||
// are virtualized.
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
selectedState,
|
||||
onSelectedChange,
|
||||
onRemoveSelectedStateItem
|
||||
} = this.props;
|
||||
|
||||
prevProps.items.forEach((prevItem) => {
|
||||
const {
|
||||
id
|
||||
} = prevItem;
|
||||
|
||||
const item = _.find(items, { id });
|
||||
|
||||
if (!item) {
|
||||
onRemoveSelectedStateItem(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedArtist = item.selectedArtist;
|
||||
const isSelected = selectedState[id];
|
||||
|
||||
const isExistingArtist = !!selectedArtist &&
|
||||
_.some(prevProps.allArtists, { foreignArtistId: selectedArtist.foreignArtistId });
|
||||
|
||||
// Props doesn't have a selected artist or
|
||||
// the selected artist is an existing artist.
|
||||
if ((!selectedArtist && prevItem.selectedArtist) || (isExistingArtist && !prevItem.selectedArtist)) {
|
||||
onSelectedChange({ id, value: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// State is selected, but a artist isn't selected or
|
||||
// the selected artist is an existing artist.
|
||||
if (isSelected && (!selectedArtist || isExistingArtist)) {
|
||||
onSelectedChange({ id, value: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A artist is being selected that wasn't previously selected.
|
||||
if (selectedArtist && selectedArtist !== prevItem.selectedArtist) {
|
||||
onSelectedChange({ id, value: true });
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
rootFolderId,
|
||||
items,
|
||||
selectedState,
|
||||
showMetadataProfile,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const item = items[rowIndex];
|
||||
|
||||
return (
|
||||
<VirtualTableRow
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<ImportArtistRowConnector
|
||||
key={item.id}
|
||||
style={style}
|
||||
rootFolderId={rootFolderId}
|
||||
showMetadataProfile={showMetadataProfile}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
id={item.id}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
isSmallScreen,
|
||||
showMetadataProfile,
|
||||
scroller,
|
||||
selectedState,
|
||||
onSelectAllChange
|
||||
} = this.props;
|
||||
|
||||
if (!items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTable
|
||||
items={items}
|
||||
isSmallScreen={isSmallScreen}
|
||||
scroller={scroller}
|
||||
rowHeight={52}
|
||||
overscanRowCount={2}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
<ImportArtistHeader
|
||||
showMetadataProfile={showMetadataProfile}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtistTable.propTypes = {
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||
defaultMonitor: PropTypes.string.isRequired,
|
||||
defaultQualityProfileId: PropTypes.number,
|
||||
defaultMetadataProfileId: PropTypes.number,
|
||||
defaultAlbumFolder: PropTypes.bool.isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
allArtists: PropTypes.arrayOf(PropTypes.object),
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onRemoveSelectedStateItem: PropTypes.func.isRequired,
|
||||
onArtistLookup: PropTypes.func.isRequired,
|
||||
onSetImportArtistValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportArtistTable;
|
|
@ -1,43 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupArtist, setImportArtistValue } from 'Store/Actions/importArtistActions';
|
||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||
import ImportArtistTable from './ImportArtistTable';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addArtist,
|
||||
(state) => state.importArtist,
|
||||
(state) => state.app.dimensions,
|
||||
createAllArtistSelector(),
|
||||
(addArtist, importArtist, dimensions, allArtists) => {
|
||||
return {
|
||||
defaultMonitor: addArtist.defaults.monitor,
|
||||
defaultQualityProfileId: addArtist.defaults.qualityProfileId,
|
||||
defaultMetadataProfileId: addArtist.defaults.metadataProfileId,
|
||||
defaultAlbumFolder: addArtist.defaults.albumFolder,
|
||||
items: importArtist.items,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
allArtists
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onArtistLookup(name, path) {
|
||||
dispatch(queueLookupArtist({
|
||||
name,
|
||||
path,
|
||||
term: name
|
||||
}));
|
||||
},
|
||||
|
||||
onSetImportArtistValue(values) {
|
||||
dispatch(setImportArtistValue(values));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(ImportArtistTable);
|
|
@ -1,19 +0,0 @@
|
|||
.artistNameContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.artistName {
|
||||
@add-mixin truncate;
|
||||
}
|
||||
|
||||
.disambiguation {
|
||||
margin-right: 5px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.existing {
|
||||
margin-left: 5px;
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Label from 'Components/Label';
|
||||
import styles from './ImportArtistName.css';
|
||||
|
||||
function ImportArtistName(props) {
|
||||
const {
|
||||
artistName,
|
||||
disambiguation,
|
||||
isExistingArtist
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.artistNameContainer}>
|
||||
<div className={styles.artistName}>
|
||||
{artistName}
|
||||
</div>
|
||||
<div className={styles.disambiguation}>
|
||||
{disambiguation}
|
||||
</div>
|
||||
|
||||
{
|
||||
isExistingArtist &&
|
||||
<Label
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
Existing
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImportArtistName.propTypes = {
|
||||
artistName: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string,
|
||||
isExistingArtist: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ImportArtistName;
|
|
@ -1,8 +0,0 @@
|
|||
.artist {
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: $menuItemHoverBackgroundColor;
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ImportArtistName from './ImportArtistName';
|
||||
import styles from './ImportArtistSearchResult.css';
|
||||
|
||||
class ImportArtistSearchResult extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.foreignArtistId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
artistName,
|
||||
disambiguation,
|
||||
// year,
|
||||
isExistingArtist
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.artist}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportArtistName
|
||||
artistName={artistName}
|
||||
disambiguation={disambiguation}
|
||||
// year={year}
|
||||
isExistingArtist={isExistingArtist}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtistSearchResult.propTypes = {
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string,
|
||||
// year: PropTypes.number.isRequired,
|
||||
isExistingArtist: PropTypes.bool.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportArtistSearchResult;
|
|
@ -1,17 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createExistingArtistSelector from 'Store/Selectors/createExistingArtistSelector';
|
||||
import ImportArtistSearchResult from './ImportArtistSearchResult';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingArtistSelector(),
|
||||
(isExistingArtist) => {
|
||||
return {
|
||||
isExistingArtist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ImportArtistSearchResult);
|
|
@ -1,77 +0,0 @@
|
|||
.button {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.existing {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
flex: 1 0 auto;
|
||||
margin-left: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
z-index: $popperZIndex;
|
||||
margin-top: 4px;
|
||||
/* 400px container witdh with 8px padding on each side */
|
||||
width: 384px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 4px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.searchIconContainer {
|
||||
width: 58px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-right: none;
|
||||
border-radius: 4px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-color: #edf1f2;
|
||||
text-align: center;
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.results {
|
||||
@add-mixin scrollbar;
|
||||
@add-mixin scrollbarTrack;
|
||||
@add-mixin scrollbarThumb;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
max-height: 165px;
|
||||
}
|
|
@ -1,303 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Portal from 'Components/Portal';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import ImportArtistSearchResultConnector from './ImportArtistSearchResultConnector';
|
||||
import ImportArtistName from './ImportArtistName';
|
||||
import styles from './ImportArtistSelectArtist.css';
|
||||
|
||||
class ImportArtistSelectArtist extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._artistLookupTimeout = null;
|
||||
this._scheduleUpdate = null;
|
||||
this._buttonId = getUniqueElememtId();
|
||||
this._contentId = getUniqueElememtId();
|
||||
|
||||
this.state = {
|
||||
term: props.id,
|
||||
isOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this._scheduleUpdate) {
|
||||
this._scheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_addListener() {
|
||||
window.addEventListener('click', this.onWindowClick);
|
||||
}
|
||||
|
||||
_removeListener() {
|
||||
window.removeEventListener('click', this.onWindowClick);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onWindowClick = (event) => {
|
||||
const button = document.getElementById(this._buttonId);
|
||||
const content = document.getElementById(this._contentId);
|
||||
|
||||
if (!button || !content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!button.contains(event.target) &&
|
||||
!content.contains(event.target) &&
|
||||
this.state.isOpen
|
||||
) {
|
||||
this.setState({ isOpen: false });
|
||||
this._removeListener();
|
||||
}
|
||||
}
|
||||
|
||||
onPress = () => {
|
||||
if (this.state.isOpen) {
|
||||
this._removeListener();
|
||||
} else {
|
||||
this._addListener();
|
||||
}
|
||||
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
|
||||
onSearchInputChange = ({ value }) => {
|
||||
if (this._artistLookupTimeout) {
|
||||
clearTimeout(this._artistLookupTimeout);
|
||||
}
|
||||
|
||||
this.setState({ term: value }, () => {
|
||||
this._artistLookupTimeout = setTimeout(() => {
|
||||
this.props.onSearchInputChange(value);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.onSearchInputChange(this.state.term);
|
||||
}
|
||||
|
||||
onArtistSelect = (foreignArtistId) => {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onArtistSelect(foreignArtistId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedArtist,
|
||||
isExistingArtist,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
isQueued,
|
||||
isLookingUpArtist
|
||||
} = this.props;
|
||||
|
||||
const errorMessage = error &&
|
||||
error.responseJSON &&
|
||||
error.responseJSON.message;
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div
|
||||
ref={ref}
|
||||
id={this._buttonId}
|
||||
>
|
||||
<Link
|
||||
ref={ref}
|
||||
className={styles.button}
|
||||
component="div"
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
isLookingUpArtist && isQueued && !isPopulated ?
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && selectedArtist && isExistingArtist ?
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && selectedArtist ?
|
||||
<ImportArtistName
|
||||
artistName={selectedArtist.artistName}
|
||||
disambiguation={selectedArtist.disambiguation}
|
||||
// year={selectedArtist.year}
|
||||
isExistingArtist={isExistingArtist}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !selectedArtist ?
|
||||
<div className={styles.noMatches}>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
No match found!
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error ?
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
title={errorMessage}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
Search failed, please try again later.
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className={styles.dropdownArrowContainer}>
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
|
||||
<Portal>
|
||||
<Popper
|
||||
placement="bottom"
|
||||
modifiers={{
|
||||
preventOverflow: {
|
||||
boundariesElement: 'viewport'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ ref, style, scheduleUpdate }) => {
|
||||
this._scheduleUpdate = scheduleUpdate;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id={this._contentId}
|
||||
className={styles.contentContainer}
|
||||
style={style}
|
||||
>
|
||||
{
|
||||
this.state.isOpen ?
|
||||
<div className={styles.content}>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<Icon name={icons.SEARCH} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
name={`${name}_textInput`}
|
||||
value={this.state.term}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
|
||||
<FormInputButton
|
||||
kind={kinds.DEFAULT}
|
||||
spinnerIcon={icons.REFRESH}
|
||||
canSpin={true}
|
||||
isSpinning={isFetching}
|
||||
onPress={this.onRefreshPress}
|
||||
>
|
||||
<Icon name={icons.REFRESH} />
|
||||
</FormInputButton>
|
||||
</div>
|
||||
|
||||
<div className={styles.results}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ImportArtistSearchResultConnector
|
||||
key={item.foreignArtistId}
|
||||
foreignArtistId={item.foreignArtistId}
|
||||
artistName={item.artistName}
|
||||
disambiguation={item.disambiguation}
|
||||
// year={item.year}
|
||||
onPress={this.onArtistSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Popper>
|
||||
</Portal>
|
||||
</Manager>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtistSelectArtist.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
selectedArtist: PropTypes.object,
|
||||
isExistingArtist: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isQueued: PropTypes.bool.isRequired,
|
||||
isLookingUpArtist: PropTypes.bool.isRequired,
|
||||
onSearchInputChange: PropTypes.func.isRequired,
|
||||
onArtistSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportArtistSelectArtist.defaultProps = {
|
||||
isFetching: true,
|
||||
isPopulated: false,
|
||||
items: [],
|
||||
isQueued: true
|
||||
};
|
||||
|
||||
export default ImportArtistSelectArtist;
|
|
@ -1,76 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupArtist, setImportArtistValue } from 'Store/Actions/importArtistActions';
|
||||
import createImportArtistItemSelector from 'Store/Selectors/createImportArtistItemSelector';
|
||||
import ImportArtistSelectArtist from './ImportArtistSelectArtist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.importArtist.isLookingUpArtist,
|
||||
createImportArtistItemSelector(),
|
||||
(isLookingUpArtist, item) => {
|
||||
return {
|
||||
isLookingUpArtist,
|
||||
...item
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
queueLookupArtist,
|
||||
setImportArtistValue
|
||||
};
|
||||
|
||||
class ImportArtistSelectArtistConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchInputChange = (term) => {
|
||||
this.props.queueLookupArtist({
|
||||
name: this.props.id,
|
||||
term,
|
||||
topOfQueue: true
|
||||
});
|
||||
}
|
||||
|
||||
onArtistSelect = (foreignArtistId) => {
|
||||
const {
|
||||
id,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
this.props.setImportArtistValue({
|
||||
id,
|
||||
selectedArtist: _.find(items, { foreignArtistId })
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportArtistSelectArtist
|
||||
{...this.props}
|
||||
onSearchInputChange={this.onSearchInputChange}
|
||||
onArtistSelect={this.onArtistSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtistSelectArtistConnector.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
selectedArtist: PropTypes.object,
|
||||
isSelected: PropTypes.bool,
|
||||
queueLookupArtist: PropTypes.func.isRequired,
|
||||
setImportArtistValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistSelectArtistConnector);
|
|
@ -1,30 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import ImportArtistSelectFolderConnector from 'AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolderConnector';
|
||||
import ImportArtistConnector from 'AddArtist/ImportArtist/Import/ImportArtistConnector';
|
||||
|
||||
class ImportArtist extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
exact={true}
|
||||
path="/add/import"
|
||||
component={ImportArtistSelectFolderConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/add/import/:rootFolderId"
|
||||
component={ImportArtistConnector}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ImportArtist;
|
|
@ -1,32 +0,0 @@
|
|||
.header {
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: $defaultFontSize;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: 12px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.recentFolders {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.startImport {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.importButtonIcon {
|
||||
margin-right: 8px;
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import RootFolders from 'RootFolder/RootFolders';
|
||||
import styles from './ImportArtistSelectFolder.css';
|
||||
|
||||
class ImportArtistSelectFolder extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddNewRootFolderModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
onAddNewRootFolderPress = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
||||
}
|
||||
|
||||
onNewRootFolderSelect = ({ value }) => {
|
||||
this.props.onNewRootFolderSelect(value);
|
||||
}
|
||||
|
||||
onAddRootFolderModalClose = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isWindows,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PageContent title="Import Artist">
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load root folders</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated &&
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
Import artist(s) you already have
|
||||
</div>
|
||||
|
||||
<div className={styles.tips}>
|
||||
Some tips to ensure the import goes smoothly:
|
||||
<ul>
|
||||
<li className={styles.tip}>
|
||||
Point Lidarr to the folder containing all of your music not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\music' : '/music'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\music\\sublime' : '/music/sublime'}"</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{
|
||||
items.length > 0 ?
|
||||
<div className={styles.recentFolders}>
|
||||
<FieldSet legend="Root Folders">
|
||||
<RootFolders
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
error={error}
|
||||
items={items}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={this.onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.importButtonIcon}
|
||||
name={icons.DRIVE}
|
||||
/>
|
||||
Choose another folder
|
||||
</Button>
|
||||
</div> :
|
||||
|
||||
<div className={styles.startImport}>
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={this.onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.importButtonIcon}
|
||||
name={icons.DRIVE}
|
||||
/>
|
||||
Start Import
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<FileBrowserModal
|
||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
||||
name="rootFolderPath"
|
||||
value=""
|
||||
onChange={this.onNewRootFolderSelect}
|
||||
onModalClose={this.onAddRootFolderModalClose}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtistSelectFolder.propTypes = {
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportArtistSelectFolder;
|
|
@ -1,84 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { push } from 'connected-react-router';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import { fetchRootFolders, addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import ImportArtistSelectFolder from './ImportArtistSelectFolder';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.rootFolders,
|
||||
createSystemStatusSelector(),
|
||||
(rootFolders, systemStatus) => {
|
||||
return {
|
||||
...rootFolders,
|
||||
isWindows: systemStatus.isWindows
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchRootFolders,
|
||||
addRootFolder,
|
||||
push
|
||||
};
|
||||
|
||||
class ImportArtistSelectFolderConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRootFolders();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isSaving,
|
||||
saveError
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
const newRootFolders = _.differenceBy(items, prevProps.items, (item) => item.id);
|
||||
|
||||
if (newRootFolders.length === 1) {
|
||||
this.props.push(`${window.Lidarr.urlBase}/add/import/${newRootFolders[0].id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onNewRootFolderSelect = (path) => {
|
||||
this.props.addRootFolder({ path });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportArtistSelectFolder
|
||||
{...this.props}
|
||||
onNewRootFolderSelect={this.onNewRootFolderSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtistSelectFolderConnector.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
addRootFolder: PropTypes.func.isRequired,
|
||||
push: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistSelectFolderConnector);
|
|
@ -25,7 +25,6 @@ class TrackRow extends Component {
|
|||
title,
|
||||
duration,
|
||||
trackFilePath,
|
||||
trackFileRelativePath,
|
||||
columns,
|
||||
deleteTrackFile
|
||||
} = this.props;
|
||||
|
@ -86,16 +85,6 @@ class TrackRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (name === 'relativePath') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
trackFileRelativePath
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'duration') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
@ -170,7 +159,6 @@ TrackRow.propTypes = {
|
|||
duration: PropTypes.number.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
trackFilePath: PropTypes.string,
|
||||
trackFileRelativePath: PropTypes.string,
|
||||
mediaInfo: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
|
|
@ -10,8 +10,7 @@ function createMapStateToProps() {
|
|||
createTrackFileSelector(),
|
||||
(id, trackFile) => {
|
||||
return {
|
||||
trackFilePath: trackFile ? trackFile.path : null,
|
||||
trackFileRelativePath: trackFile ? trackFile.relativePath : null
|
||||
trackFilePath: trackFile ? trackFile.path : null
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,7 +6,6 @@ import NotFound from 'Components/NotFound';
|
|||
import Switch from 'Components/Router/Switch';
|
||||
import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector';
|
||||
import AddNewItemConnector from 'Search/AddNewItemConnector';
|
||||
import ImportArtist from 'AddArtist/ImportArtist/ImportArtist';
|
||||
import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector';
|
||||
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
|
||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||
|
@ -76,11 +75,6 @@ function AppRoutes(props) {
|
|||
component={AddNewItemConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/add/import"
|
||||
component={ImportArtist}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/artisteditor"
|
||||
component={ArtistEditorConnector}
|
||||
|
|
|
@ -27,7 +27,7 @@ function ConnectionLostModal(props) {
|
|||
|
||||
<ModalBody>
|
||||
<div>
|
||||
Lidarr has lost it's connection to the backend and will need to be reloaded to restore functionality.
|
||||
Lidarr has lost its connection to the backend and will need to be reloaded to restore functionality.
|
||||
</div>
|
||||
|
||||
<div className={styles.automatic}>
|
||||
|
|
|
@ -13,8 +13,7 @@ function createMapStateToProps() {
|
|||
return {
|
||||
foreignArtistId: artist.foreignArtistId,
|
||||
artistMonitored: artist.monitored,
|
||||
trackFilePath: trackFile ? trackFile.path : null,
|
||||
trackFileRelativePath: trackFile ? trackFile.relativePath : null
|
||||
trackFilePath: trackFile ? trackFile.path : null
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
|
|||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/settingsActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import ArtistEditor from './ArtistEditor';
|
||||
|
|
|
@ -217,6 +217,7 @@ class ArtistEditorFooter extends Component {
|
|||
name="metadataProfileId"
|
||||
value={metadataProfileId}
|
||||
includeNoChange={true}
|
||||
includeNone={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
|
|
|
@ -217,7 +217,6 @@ class ArtistIndex extends Component {
|
|||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isRefreshingArtist}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={onRefreshArtistPress}
|
||||
/>
|
||||
|
||||
|
|
|
@ -20,15 +20,15 @@ function NoArtist(props) {
|
|||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
No artist found, to get started you'll want to add a new artist or album or import some existing ones.
|
||||
No artists found, to get started you'll want to add a new artist or album or add an existing library location (Root Folder) and update.
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
to="/add/import"
|
||||
to="/settings/mediamanagement"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
Import Existing Artist(s)
|
||||
Add Root Folder
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ export const MOVE_ARTIST = 'MoveArtist';
|
|||
export const REFRESH_ARTIST = 'RefreshArtist';
|
||||
export const RENAME_FILES = 'RenameFiles';
|
||||
export const RENAME_ARTIST = 'RenameArtist';
|
||||
export const RESCAN_FOLDERS = 'RescanFolders';
|
||||
export const RETAG_FILES = 'RetagFiles';
|
||||
export const RETAG_ARTIST = 'RetagArtist';
|
||||
export const RESET_API_KEY = 'ResetApiKey';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import EditRootFolderModalConnector from 'Settings/MediaManagement/RootFolder/EditRootFolderModalConnector';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import RootFolderSelectInputOption from './RootFolderSelectInputOption';
|
||||
import RootFolderSelectInputSelectedValue from './RootFolderSelectInputSelectedValue';
|
||||
|
@ -14,8 +14,7 @@ class RootFolderSelectInput extends Component {
|
|||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddNewRootFolderModalOpen: false,
|
||||
newRootFolderPath: ''
|
||||
isAddNewRootFolderModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -52,9 +51,7 @@ class RootFolderSelectInput extends Component {
|
|||
}
|
||||
|
||||
onNewRootFolderSelect = ({ value }) => {
|
||||
this.setState({ newRootFolderPath: value }, () => {
|
||||
this.props.onNewRootFolderSelect(value);
|
||||
});
|
||||
this.setState({ newRootFolderPath: value });
|
||||
}
|
||||
|
||||
onAddRootFolderModalClose = () => {
|
||||
|
@ -66,8 +63,7 @@ class RootFolderSelectInput extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
includeNoChange,
|
||||
onNewRootFolderSelect,
|
||||
value,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
|
@ -75,17 +71,16 @@ class RootFolderSelectInput extends Component {
|
|||
<div>
|
||||
<EnhancedSelectInput
|
||||
{...otherProps}
|
||||
value={value || ''}
|
||||
selectedValueComponent={RootFolderSelectInputSelectedValue}
|
||||
optionComponent={RootFolderSelectInputOption}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
|
||||
<FileBrowserModal
|
||||
<EditRootFolderModalConnector
|
||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
||||
name="rootFolderPath"
|
||||
value=""
|
||||
onChange={this.onNewRootFolderSelect}
|
||||
onModalClose={this.onAddRootFolderModalClose}
|
||||
onRootFolderAdded={this.onNewRootFolderSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -94,16 +89,11 @@ class RootFolderSelectInput extends Component {
|
|||
|
||||
RootFolderSelectInput.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
RootFolderSelectInput.defaultProps = {
|
||||
includeNoChange: false
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RootFolderSelectInput;
|
||||
|
|
|
@ -2,20 +2,20 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import RootFolderSelectInput from './RootFolderSelectInput';
|
||||
|
||||
const ADD_NEW_KEY = 'addNew';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.rootFolders,
|
||||
(state) => state.settings.rootFolders,
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(rootFolders, includeNoChange) => {
|
||||
const values = rootFolders.items.map((rootFolder) => {
|
||||
return {
|
||||
key: rootFolder.path,
|
||||
value: rootFolder.path,
|
||||
name: rootFolder.name,
|
||||
freeSpace: rootFolder.freeSpace
|
||||
};
|
||||
});
|
||||
|
@ -23,7 +23,8 @@ function createMapStateToProps() {
|
|||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
value: '',
|
||||
name: 'No Change',
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
@ -32,6 +33,7 @@ function createMapStateToProps() {
|
|||
values.push({
|
||||
key: '',
|
||||
value: '',
|
||||
name: '',
|
||||
isDisabled: true,
|
||||
isHidden: true
|
||||
});
|
||||
|
@ -39,7 +41,8 @@ function createMapStateToProps() {
|
|||
|
||||
values.push({
|
||||
key: ADD_NEW_KEY,
|
||||
value: 'Add a new path'
|
||||
value: '',
|
||||
name: 'Add a new path'
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -51,14 +54,6 @@ function createMapStateToProps() {
|
|||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchAddRootFolder(path) {
|
||||
dispatch(addRootFolder({ path }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class RootFolderSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
|
@ -95,19 +90,11 @@ class RootFolderSelectInputConnector extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onNewRootFolderSelect = (path) => {
|
||||
this.props.dispatchAddRootFolder(path);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchAddRootFolder,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
|
@ -125,12 +112,11 @@ RootFolderSelectInputConnector.propTypes = {
|
|||
value: PropTypes.string,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dispatchAddRootFolder: PropTypes.func.isRequired
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
RootFolderSelectInputConnector.defaultProps = {
|
||||
includeNoChange: false
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(RootFolderSelectInputConnector);
|
||||
export default connect(createMapStateToProps)(RootFolderSelectInputConnector);
|
||||
|
|
|
@ -8,11 +8,14 @@ import styles from './RootFolderSelectInputOption.css';
|
|||
function RootFolderSelectInputOption(props) {
|
||||
const {
|
||||
value,
|
||||
name,
|
||||
freeSpace,
|
||||
isMobile,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const text = value === '' ? name : `${name} [${value}]`;
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputOption
|
||||
isMobile={isMobile}
|
||||
|
@ -23,7 +26,7 @@ function RootFolderSelectInputOption(props) {
|
|||
isMobile && styles.isMobile
|
||||
)}
|
||||
>
|
||||
<div>{value}</div>
|
||||
<div>{text}</div>
|
||||
|
||||
{
|
||||
freeSpace != null &&
|
||||
|
@ -37,6 +40,7 @@ function RootFolderSelectInputOption(props) {
|
|||
}
|
||||
|
||||
RootFolderSelectInputOption.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
freeSpace: PropTypes.number,
|
||||
isMobile: PropTypes.bool.isRequired
|
||||
|
|
|
@ -6,19 +6,22 @@ import styles from './RootFolderSelectInputSelectedValue.css';
|
|||
|
||||
function RootFolderSelectInputSelectedValue(props) {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
freeSpace,
|
||||
includeFreeSpace,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const text = value === '' ? name : `${name} [${value}]`;
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputSelectedValue
|
||||
className={styles.selectedValue}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.path}>
|
||||
{value}
|
||||
{text}
|
||||
</div>
|
||||
|
||||
{
|
||||
|
@ -32,6 +35,7 @@ function RootFolderSelectInputSelectedValue(props) {
|
|||
}
|
||||
|
||||
RootFolderSelectInputSelectedValue.propTypes = {
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
freeSpace: PropTypes.number,
|
||||
includeFreeSpace: PropTypes.bool.isRequired
|
||||
|
|
|
@ -75,7 +75,7 @@ SelectInput.propTypes = {
|
|||
className: PropTypes.string,
|
||||
disabledClassName: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
|
|
|
@ -17,6 +17,8 @@ function getIconName(name) {
|
|||
return icons.SEARCH;
|
||||
case 'Housekeeping':
|
||||
return icons.HOUSEKEEPING;
|
||||
case 'RescanFolders':
|
||||
return icons.RESCAN;
|
||||
case 'RefreshArtist':
|
||||
return icons.REFRESH;
|
||||
case 'RssSync':
|
||||
|
|
|
@ -28,10 +28,6 @@ const links = [
|
|||
title: 'Add New',
|
||||
to: '/add/search'
|
||||
},
|
||||
{
|
||||
title: 'Import',
|
||||
to: '/add/import'
|
||||
},
|
||||
{
|
||||
title: 'Mass Editor',
|
||||
to: '/artisteditor'
|
||||
|
|
|
@ -11,7 +11,7 @@ import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
|
|||
import { fetchArtist } from 'Store/Actions/artistActions';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/settingsActions';
|
||||
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
|
||||
function getHandlerName(name) {
|
||||
|
@ -275,8 +275,14 @@ class SignalRConnector extends Component {
|
|||
// No-op for now, we may want this later
|
||||
}
|
||||
|
||||
handleRootfolder = () => {
|
||||
this.props.dispatchFetchRootFolders();
|
||||
handleRootfolder = (body) => {
|
||||
if (body.action === 'updated') {
|
||||
this.props.dispatchUpdateItem({
|
||||
section: 'settings.rootFolders',
|
||||
updateOnly: true,
|
||||
...body.resource
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleTag = (body) => {
|
||||
|
|
|
@ -82,6 +82,7 @@ import {
|
|||
faRocket as fasRocket,
|
||||
faSave as fasSave,
|
||||
faSearch as fasSearch,
|
||||
faSearchPlus as fasSearchPlus,
|
||||
faSignOutAlt as fasSignOutAlt,
|
||||
faSitemap as fasSitemap,
|
||||
faSpinner as fasSpinner,
|
||||
|
@ -110,6 +111,7 @@ import {
|
|||
export const ACTIONS = fasBolt;
|
||||
export const ACTIVITY = farClock;
|
||||
export const ADD = fasPlus;
|
||||
export const ADD_MISSING_ARTISTS = fasSearchPlus;
|
||||
export const ALTERNATE_TITLES = farClone;
|
||||
export const ADVANCED_SETTINGS = fasCog;
|
||||
export const ARROW_LEFT = fasArrowCircleLeft;
|
||||
|
@ -182,6 +184,7 @@ export const QUICK = fasRocket;
|
|||
export const REFRESH = fasSync;
|
||||
export const REMOVE = fasTimes;
|
||||
export const REORDER = fasBars;
|
||||
export const RESCAN = fasFolderOpen;
|
||||
export const RESTART = fasRedoAlt;
|
||||
export const RESTORE = fasHistory;
|
||||
export const RETAG = fasEdit;
|
||||
|
|
|
@ -30,8 +30,8 @@ import styles from './InteractiveImportModalContent.css';
|
|||
|
||||
const columns = [
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: 'Relative Path',
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.relativePath {
|
||||
.path {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
word-break: break-all;
|
||||
|
|
|
@ -149,7 +149,7 @@ class InteractiveImportRow extends Component {
|
|||
const {
|
||||
id,
|
||||
allowArtistChange,
|
||||
relativePath,
|
||||
path,
|
||||
artist,
|
||||
album,
|
||||
albumReleaseId,
|
||||
|
@ -190,7 +190,7 @@ class InteractiveImportRow extends Component {
|
|||
|
||||
const pathCellContents = (
|
||||
<div>
|
||||
{relativePath}
|
||||
{path}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -213,8 +213,8 @@ class InteractiveImportRow extends Component {
|
|||
/>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.relativePath}
|
||||
title={relativePath}
|
||||
className={styles.path}
|
||||
title={path}
|
||||
>
|
||||
{pathCell}
|
||||
</TableRowCell>
|
||||
|
@ -328,7 +328,7 @@ class InteractiveImportRow extends Component {
|
|||
audioTags={audioTags}
|
||||
sortKey='mediumNumber'
|
||||
sortDirection={sortDirections.ASCENDING}
|
||||
filename={relativePath}
|
||||
filename={path}
|
||||
onModalClose={this.onSelectTrackModalClose}
|
||||
/>
|
||||
|
||||
|
@ -349,7 +349,7 @@ class InteractiveImportRow extends Component {
|
|||
InteractiveImportRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
allowArtistChange: PropTypes.bool.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
artist: PropTypes.object,
|
||||
album: PropTypes.object,
|
||||
albumReleaseId: PropTypes.number,
|
||||
|
|
|
@ -75,7 +75,6 @@ class OrganizePreviewModalContent extends Component {
|
|||
error,
|
||||
items,
|
||||
trackFormat,
|
||||
path,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
|
@ -113,13 +112,6 @@ class OrganizePreviewModalContent extends Component {
|
|||
!isFetching && isPopulated && !!items.length &&
|
||||
<div>
|
||||
<Alert>
|
||||
<div>
|
||||
All paths are relative to:
|
||||
<span className={styles.path}>
|
||||
{path}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Naming pattern:
|
||||
<span className={styles.trackFormat}>
|
||||
|
|
|
@ -74,7 +74,6 @@ class RetagPreviewModalContent extends Component {
|
|||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
path,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
|
@ -112,12 +111,6 @@ class RetagPreviewModalContent extends Component {
|
|||
!isFetching && isPopulated && !!items.length &&
|
||||
<div>
|
||||
<Alert>
|
||||
<div>
|
||||
All paths are relative to:
|
||||
<span className={styles.path}>
|
||||
{path}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
MusicBrainz identifiers will also be added to the files; these are not shown below.
|
||||
</div>
|
||||
|
@ -130,7 +123,7 @@ class RetagPreviewModalContent extends Component {
|
|||
<RetagPreviewRow
|
||||
key={item.trackFileId}
|
||||
id={item.trackFileId}
|
||||
path={item.relativePath}
|
||||
path={item.path}
|
||||
changes={item.changes}
|
||||
isSelected={selectedState[item.trackFileId]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
}
|
||||
|
||||
.unavailablePath {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.unavailableLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.freeSpace,
|
||||
.unmappedFolders {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 45px;
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import styles from './RootFolderRow.css';
|
||||
|
||||
function RootFolderRow(props) {
|
||||
const {
|
||||
id,
|
||||
path,
|
||||
accessible,
|
||||
freeSpace,
|
||||
unmappedFolders,
|
||||
onDeletePress
|
||||
} = props;
|
||||
|
||||
const isUnavailable = !accessible;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>
|
||||
{
|
||||
isUnavailable ?
|
||||
<div className={styles.unavailablePath}>
|
||||
{path}
|
||||
|
||||
<Label
|
||||
className={styles.unavailableLabel}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
Unavailable
|
||||
</Label>
|
||||
</div> :
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`/add/import/${id}`}
|
||||
>
|
||||
{path}
|
||||
</Link>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.freeSpace}>
|
||||
{(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.unmappedFolders}>
|
||||
{isUnavailable ? '-' : unmappedFolders.length}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
title="Remove root folder"
|
||||
name={icons.REMOVE}
|
||||
onPress={onDeletePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
RootFolderRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
accessible: PropTypes.bool.isRequired,
|
||||
freeSpace: PropTypes.number,
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
RootFolderRow.defaultProps = {
|
||||
unmappedFolders: []
|
||||
};
|
||||
|
||||
export default RootFolderRow;
|
|
@ -1,13 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { deleteRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import RootFolderRow from './RootFolderRow';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onDeletePress() {
|
||||
dispatch(deleteRootFolder({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(RootFolderRow);
|
|
@ -1,81 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import RootFolderRowConnector from './RootFolderRowConnector';
|
||||
|
||||
const rootFolderColumns = [
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'freeSpace',
|
||||
label: 'Free Space',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'unmappedFolders',
|
||||
label: 'Unmapped Folders',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function RootFolders(props) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = props;
|
||||
|
||||
if (isFetching && !isPopulated) {
|
||||
return (
|
||||
<LoadingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFetching && !!error) {
|
||||
return (
|
||||
<div>Unable to load root folders</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={rootFolderColumns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((rootFolder) => {
|
||||
return (
|
||||
<RootFolderRowConnector
|
||||
key={rootFolder.id}
|
||||
id={rootFolder.id}
|
||||
path={rootFolder.path}
|
||||
accessible={rootFolder.accessible}
|
||||
freeSpace={rootFolder.freeSpace}
|
||||
unmappedFolders={rootFolder.unmappedFolders}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
RootFolders.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default RootFolders;
|
|
@ -1,46 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import RootFolders from './RootFolders';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.rootFolders,
|
||||
(rootFolders) => {
|
||||
return rootFolders;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchRootFolders: fetchRootFolders
|
||||
};
|
||||
|
||||
class RootFoldersConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RootFolders
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RootFoldersConnector.propTypes = {
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(RootFoldersConnector);
|
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
import { getSearchResults, clearSearchResults } from 'Store/Actions/searchActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/settingsActions';
|
||||
import AddNewItem from './AddNewItem';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
|
|
@ -4,8 +4,7 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import { fetchImportLists, deleteImportList } from 'Store/Actions/settingsActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchImportLists, deleteImportList, fetchRootFolders } from 'Store/Actions/settingsActions';
|
||||
import ImportLists from './ImportLists';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
|
|
@ -10,9 +10,8 @@ import Form from 'Components/Form/Form';
|
|||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import RootFoldersConnector from 'RootFolder/RootFoldersConnector';
|
||||
import RootFoldersConnector from './RootFolder/RootFoldersConnector';
|
||||
import NamingConnector from './Naming/NamingConnector';
|
||||
import AddRootFolderConnector from './RootFolder/AddRootFolderConnector';
|
||||
|
||||
const rescanAfterRefreshOptions = [
|
||||
{ key: 'always', value: 'Always' },
|
||||
|
@ -64,6 +63,7 @@ class MediaManagement extends Component {
|
|||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<RootFoldersConnector />
|
||||
<NamingConnector />
|
||||
|
||||
{
|
||||
|
@ -427,11 +427,6 @@ class MediaManagement extends Component {
|
|||
}
|
||||
</Form>
|
||||
}
|
||||
|
||||
<FieldSet legend="Root Folders">
|
||||
<RootFoldersConnector />
|
||||
<AddRootFolderConnector />
|
||||
</FieldSet>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
.addRootFolderButtonContainer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.importButtonIcon {
|
||||
margin-right: 8px;
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Icon from 'Components/Icon';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import styles from './AddRootFolder.css';
|
||||
|
||||
class AddRootFolder extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddNewRootFolderModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
onAddNewRootFolderPress = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
||||
}
|
||||
|
||||
onNewRootFolderSelect = ({ value }) => {
|
||||
this.props.onNewRootFolderSelect(value);
|
||||
}
|
||||
|
||||
onAddRootFolderModalClose = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.addRootFolderButtonContainer}>
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={this.onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.importButtonIcon}
|
||||
name={icons.DRIVE}
|
||||
/>
|
||||
Add Root Folder
|
||||
</Button>
|
||||
|
||||
<FileBrowserModal
|
||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
||||
name="rootFolderPath"
|
||||
value=""
|
||||
onChange={this.onNewRootFolderSelect}
|
||||
onModalClose={this.onAddRootFolderModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddRootFolder.propTypes = {
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddRootFolder;
|
|
@ -1,13 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import AddRootFolder from './AddRootFolder';
|
||||
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
|
||||
function createMapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onNewRootFolderSelect(path) {
|
||||
dispatch(addRootFolder({ path }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(AddRootFolder);
|
|
@ -0,0 +1,25 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditRootFolderModalContentConnector from './EditRootFolderModalContentConnector';
|
||||
|
||||
function EditRootFolderModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditRootFolderModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditRootFolderModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditRootFolderModal;
|
|
@ -0,0 +1,58 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { cancelSaveRootFolder } from 'Store/Actions/settingsActions';
|
||||
import EditRootFolderModal from './EditRootFolderModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const section = 'settings.rootFolders';
|
||||
|
||||
return {
|
||||
dispatchClearPendingChanges() {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelSaveRootFolder() {
|
||||
dispatch(cancelSaveRootFolder({ section }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EditRootFolderModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPendingChanges();
|
||||
this.props.dispatchCancelSaveRootFolder();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchCancelSaveRootFolder,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EditRootFolderModal
|
||||
{...otherProps}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditRootFolderModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchCancelSaveRootFolder: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(EditRootFolderModalConnector);
|
|
@ -0,0 +1,15 @@
|
|||
.deleteButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hideMetadataProfile {
|
||||
composes: group from '~Components/Form/FormGroup.css';
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
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 Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent';
|
||||
import ArtistMetadataProfilePopoverContent from 'AddArtist/ArtistMetadataProfilePopoverContent';
|
||||
import styles from './EditRootFolderModalContent.css';
|
||||
|
||||
function EditRootFolderModalContent(props) {
|
||||
|
||||
const {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onDeleteRootFolderPress,
|
||||
showMetadataProfile,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
path,
|
||||
defaultQualityProfileId,
|
||||
defaultMetadataProfileId,
|
||||
defaultMonitorOption,
|
||||
defaultTags
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id ? 'Edit Root Folder' : 'Add Root Folder'}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to add a new root folder, please try again.</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Name</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Path</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={id ? inputTypes.TEXT : inputTypes.PATH}
|
||||
readOnly={!!id}
|
||||
name="path"
|
||||
helpText="Root Folder containing your music library"
|
||||
helpTextWarning="This must be different to the directory where your download client puts files"
|
||||
{...path}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Monitor
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title="Monitoring Options"
|
||||
body={<ArtistMonitoringOptionsPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_ALBUMS_SELECT}
|
||||
name="defaultMonitorOption"
|
||||
onChange={onInputChange}
|
||||
{...defaultMonitorOption}
|
||||
helpText="Default Monitoring Options for albums by artists detected in this folder"
|
||||
/>
|
||||
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Quality Profile</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="defaultQualityProfileId"
|
||||
helpText="Default Quality Profile for artists detected in this folder"
|
||||
{...defaultQualityProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
|
||||
<FormLabel>
|
||||
Metadata Profile
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title="Metadata Profile"
|
||||
body={<ArtistMetadataProfilePopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.METADATA_PROFILE_SELECT}
|
||||
name="defaultMetadataProfileId"
|
||||
helpText="Default Metadata Profile for artists detected in this folder"
|
||||
{...defaultMetadataProfileId}
|
||||
includeNone={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Default Lidarr Tags</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="defaultTags"
|
||||
helpText="Default Lidarr Tags for artists detected in this folder"
|
||||
{...defaultTags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteRootFolderPress}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
Save
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
EditRootFolderModalContent.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onDeleteRootFolderPress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditRootFolderModalContent;
|
|
@ -0,0 +1,84 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import { setRootFolderValue, saveRootFolder } from 'Store/Actions/settingsActions';
|
||||
import EditRootFolderModalContent from './EditRootFolderModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
(state) => state.settings.rootFolders,
|
||||
createProviderSettingsSelector('rootFolders'),
|
||||
(id, advancedSettings, metadataProfiles, rootFolders, rootFolderSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
showMetadataProfile: metadataProfiles.items.length > 1,
|
||||
...rootFolderSettings,
|
||||
isFetching: rootFolders.isFetching
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setRootFolderValue,
|
||||
saveRootFolder
|
||||
};
|
||||
|
||||
class EditRootFolderModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setRootFolderValue({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveRootFolder({ id: this.props.id });
|
||||
|
||||
if (this.props.onRootFolderAdded) {
|
||||
this.props.onRootFolderAdded(this.props.item.path);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditRootFolderModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditRootFolderModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setRootFolderValue: PropTypes.func.isRequired,
|
||||
saveRootFolder: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onRootFolderAdded: PropTypes.func
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditRootFolderModalContentConnector);
|
|
@ -0,0 +1,19 @@
|
|||
.rootFolder {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 290px;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.enabled {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import EditRootFolderModalConnector from './EditRootFolderModalConnector';
|
||||
import styles from './RootFolder.css';
|
||||
|
||||
class RootFolder extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditRootFolderModalOpen: false,
|
||||
isDeleteRootFolderModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditRootFolderPress = () => {
|
||||
this.setState({ isEditRootFolderModalOpen: true });
|
||||
}
|
||||
|
||||
onEditRootFolderModalClose = () => {
|
||||
this.setState({ isEditRootFolderModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteRootFolderPress = () => {
|
||||
this.setState({
|
||||
isEditRootFolderModalOpen: false,
|
||||
isDeleteRootFolderModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteRootFolderModalClose= () => {
|
||||
this.setState({ isDeleteRootFolderModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeleteRootFolder = () => {
|
||||
this.props.onConfirmDeleteRootFolder(this.props.id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
path,
|
||||
qualityProfile,
|
||||
metadataProfile
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.rootFolder}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditRootFolderPress}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{path}
|
||||
</Label>
|
||||
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{qualityProfile.name}
|
||||
</Label>
|
||||
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{metadataProfile.name}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<EditRootFolderModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditRootFolderModalOpen}
|
||||
onModalClose={this.onEditRootFolderModalClose}
|
||||
onDeleteRootFolderPress={this.onDeleteRootFolderPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteRootFolderModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Root Folder"
|
||||
message={`Are you sure you want to delete the root folder '${name}'?`}
|
||||
confirmLabel="Delete"
|
||||
onConfirm={this.onConfirmDeleteRootFolder}
|
||||
onCancel={this.onDeleteRootFolderModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RootFolder.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
metadataProfile: PropTypes.object.isRequired,
|
||||
onConfirmDeleteRootFolder: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RootFolder;
|
|
@ -0,0 +1,20 @@
|
|||
.rootFolders {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.addRootFolder {
|
||||
composes: rootFolder from '~./RootFolder.css';
|
||||
|
||||
background-color: $cardAlternateBackgroundColor;
|
||||
color: $gray;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 0;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Card from 'Components/Card';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import RootFolder from './RootFolder';
|
||||
import EditRootFolderModalConnector from './EditRootFolderModalConnector';
|
||||
import styles from './RootFolders.css';
|
||||
|
||||
class RootFolders extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddRootFolderModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddRootFolderPress = () => {
|
||||
this.setState({ isAddRootFolderModalOpen: true });
|
||||
}
|
||||
|
||||
onAddRootFolderModalClose = () => {
|
||||
this.setState({ isAddRootFolderModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
qualityProfiles,
|
||||
metadataProfiles,
|
||||
onConfirmDeleteRootFolder,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Root Folders">
|
||||
<PageSectionContent
|
||||
errorMessage="Unable to load root folders"
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.rootFolders}>
|
||||
{
|
||||
items.sort(sortByName).map((item) => {
|
||||
const qualityProfile = qualityProfiles.find((profile) => profile.id === item.defaultQualityProfileId);
|
||||
const metadataProfile = metadataProfiles.find((profile) => profile.id === item.defaultMetadataProfileId);
|
||||
return (
|
||||
<RootFolder
|
||||
key={item.id}
|
||||
{...item}
|
||||
qualityProfile={qualityProfile}
|
||||
metadataProfile={metadataProfile}
|
||||
onConfirmDeleteRootFolder={onConfirmDeleteRootFolder}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addRootFolder}
|
||||
onPress={this.onAddRootFolderPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<EditRootFolderModalConnector
|
||||
isOpen={this.state.isAddRootFolderModalOpen}
|
||||
onModalClose={this.onAddRootFolderModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RootFolders.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
metadataProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteRootFolder: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RootFolders;
|
|
@ -0,0 +1,62 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchRootFolders, deleteRootFolder } from 'Store/Actions/settingsActions';
|
||||
import RootFolders from './RootFolders';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.rootFolders,
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
(rootFolders, quality, metadata) => {
|
||||
return {
|
||||
qualityProfiles: quality.items,
|
||||
metadataProfiles: metadata.items,
|
||||
...rootFolders
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchDeleteRootFolder: deleteRootFolder
|
||||
};
|
||||
|
||||
class RootFoldersConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteRootFolder = (id) => {
|
||||
this.props.dispatchDeleteRootFolder({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RootFolders
|
||||
{...this.props}
|
||||
onConfirmDeleteRootFolder={this.onConfirmDeleteRootFolder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RootFoldersConnector.propTypes = {
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchDeleteRootFolder: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(RootFoldersConnector);
|
|
@ -0,0 +1,76 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'settings.rootFolders';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_ROOT_FOLDERS = 'settings/rootFolders/fetchRootFolders';
|
||||
export const SET_ROOT_FOLDER_VALUE = 'settings/rootFolders/setRootFolderValue';
|
||||
export const SAVE_ROOT_FOLDER = 'settings/rootFolders/saveRootFolder';
|
||||
export const CANCEL_SAVE_ROOT_FOLDER = 'settings/rootFolders/cancelSaveRootFolder';
|
||||
export const DELETE_ROOT_FOLDER = 'settings/rootFolders/deleteRootFolder';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchRootFolders = createThunk(FETCH_ROOT_FOLDERS);
|
||||
export const saveRootFolder = createThunk(SAVE_ROOT_FOLDER);
|
||||
export const cancelSaveRootFolder = createThunk(CANCEL_SAVE_ROOT_FOLDER);
|
||||
export const deleteRootFolder = createThunk(DELETE_ROOT_FOLDER);
|
||||
|
||||
export const setRootFolderValue = createAction(SET_ROOT_FOLDER_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
schema: {
|
||||
defaultTags: []
|
||||
},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
|
||||
[FETCH_ROOT_FOLDERS]: createFetchHandler(section, '/rootFolder'),
|
||||
|
||||
[SAVE_ROOT_FOLDER]: createSaveProviderHandler(section, '/rootFolder'),
|
||||
[CANCEL_SAVE_ROOT_FOLDER]: createCancelSaveProviderHandler(section),
|
||||
[DELETE_ROOT_FOLDER]: createRemoveItemHandler(section, '/rootFolder')
|
||||
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_ROOT_FOLDER_VALUE]: createSetSettingValueReducer(section)
|
||||
}
|
||||
};
|
|
@ -1,327 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import getNewArtist from 'Utilities/Artist/getNewArtist';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { set, removeItem, updateItem } from './baseActions';
|
||||
import { fetchRootFolders } from './rootFolderActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'importArtist';
|
||||
let concurrentLookups = 0;
|
||||
let abortCurrentLookup = null;
|
||||
const queue = [];
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isLookingUpArtist: false,
|
||||
isImporting: false,
|
||||
isImported: false,
|
||||
importError: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const QUEUE_LOOKUP_ARTIST = 'importArtist/queueLookupArtist';
|
||||
export const START_LOOKUP_ARTIST = 'importArtist/startLookupArtist';
|
||||
export const CANCEL_LOOKUP_ARTIST = 'importArtist/cancelLookupArtist';
|
||||
export const LOOKUP_UNSEARCHED_ARTIST = 'importArtist/lookupUnsearchedArtist';
|
||||
export const CLEAR_IMPORT_ARTIST = 'importArtist/clearImportArtist';
|
||||
export const SET_IMPORT_ARTIST_VALUE = 'importArtist/setImportArtistValue';
|
||||
export const IMPORT_ARTIST = 'importArtist/importArtist';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const queueLookupArtist = createThunk(QUEUE_LOOKUP_ARTIST);
|
||||
export const startLookupArtist = createThunk(START_LOOKUP_ARTIST);
|
||||
export const importArtist = createThunk(IMPORT_ARTIST);
|
||||
export const lookupUnsearchedArtist = createThunk(LOOKUP_UNSEARCHED_ARTIST);
|
||||
export const clearImportArtist = createAction(CLEAR_IMPORT_ARTIST);
|
||||
export const cancelLookupArtist = createAction(CANCEL_LOOKUP_ARTIST);
|
||||
|
||||
export const setImportArtistValue = createAction(SET_IMPORT_ARTIST_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[QUEUE_LOOKUP_ARTIST]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
name,
|
||||
path,
|
||||
term,
|
||||
topOfQueue = false
|
||||
} = payload;
|
||||
|
||||
const state = getState().importArtist;
|
||||
const item = _.find(state.items, { id: name }) || {
|
||||
id: name,
|
||||
term,
|
||||
path,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
...item,
|
||||
term,
|
||||
isQueued: true,
|
||||
items: []
|
||||
}));
|
||||
|
||||
const itemIndex = queue.indexOf(item.id);
|
||||
|
||||
if (itemIndex >= 0) {
|
||||
queue.splice(itemIndex, 1);
|
||||
}
|
||||
|
||||
if (topOfQueue) {
|
||||
queue.unshift(item.id);
|
||||
} else {
|
||||
queue.push(item.id);
|
||||
}
|
||||
|
||||
if (term && term.length > 2) {
|
||||
dispatch(startLookupArtist({ start: true }));
|
||||
}
|
||||
},
|
||||
|
||||
[START_LOOKUP_ARTIST]: function(getState, payload, dispatch) {
|
||||
if (concurrentLookups >= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getState().importArtist;
|
||||
|
||||
const {
|
||||
isLookingUpArtist,
|
||||
items
|
||||
} = state;
|
||||
|
||||
const queueId = queue[0];
|
||||
|
||||
if (payload.start && !isLookingUpArtist) {
|
||||
dispatch(set({ section, isLookingUpArtist: true }));
|
||||
} else if (!isLookingUpArtist) {
|
||||
return;
|
||||
} else if (!queueId) {
|
||||
dispatch(set({ section, isLookingUpArtist: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
concurrentLookups++;
|
||||
queue.splice(0, 1);
|
||||
|
||||
const queued = items.find((i) => i.id === queueId);
|
||||
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id: queued.id,
|
||||
isFetching: true
|
||||
}));
|
||||
|
||||
const { request, abortRequest } = createAjaxRequest({
|
||||
url: '/artist/lookup',
|
||||
data: {
|
||||
term: queued.term
|
||||
}
|
||||
});
|
||||
|
||||
abortCurrentLookup = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id: queued.id,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null,
|
||||
items: data,
|
||||
isQueued: false,
|
||||
selectedArtist: queued.selectedArtist || data[0],
|
||||
updateOnly: true
|
||||
}));
|
||||
});
|
||||
|
||||
request.fail((xhr) => {
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id: queued.id,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr,
|
||||
isQueued: false,
|
||||
updateOnly: true
|
||||
}));
|
||||
});
|
||||
|
||||
request.always(() => {
|
||||
concurrentLookups--;
|
||||
|
||||
dispatch(startLookupArtist());
|
||||
});
|
||||
},
|
||||
|
||||
[LOOKUP_UNSEARCHED_ARTIST]: function(getState, payload, dispatch) {
|
||||
const state = getState().importArtist;
|
||||
|
||||
if (state.isLookingUpArtist) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.items.forEach((item) => {
|
||||
const id = item.id;
|
||||
|
||||
if (
|
||||
!item.isPopulated &&
|
||||
!queue.includes(id)
|
||||
) {
|
||||
queue.push(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (queue.length) {
|
||||
dispatch(startLookupArtist({ start: true }));
|
||||
}
|
||||
},
|
||||
|
||||
[IMPORT_ARTIST]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isImporting: true }));
|
||||
|
||||
const ids = payload.ids;
|
||||
const items = getState().importArtist.items;
|
||||
const addedIds = [];
|
||||
|
||||
const allNewArtist = ids.reduce((acc, id) => {
|
||||
const item = _.find(items, { id });
|
||||
const selectedArtist = item.selectedArtist;
|
||||
|
||||
// Make sure we have a selected artist and
|
||||
// the same artist hasn't been added yet.
|
||||
if (selectedArtist && !_.some(acc, { foreignArtistId: selectedArtist.foreignArtistId })) {
|
||||
const newArtist = getNewArtist(_.cloneDeep(selectedArtist), item);
|
||||
newArtist.path = item.path;
|
||||
|
||||
addedIds.push(id);
|
||||
acc.push(newArtist);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/artist/import',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(allNewArtist)
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
set({
|
||||
section,
|
||||
isImporting: false,
|
||||
isImported: true
|
||||
}),
|
||||
|
||||
...data.map((artist) => updateItem({ section: 'artist', ...artist })),
|
||||
|
||||
...addedIds.map((id) => removeItem({ section, id }))
|
||||
]));
|
||||
|
||||
dispatch(fetchRootFolders());
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(batchActions(
|
||||
set({
|
||||
section,
|
||||
isImporting: false,
|
||||
isImported: true
|
||||
}),
|
||||
|
||||
addedIds.map((id) => updateItem({
|
||||
section,
|
||||
id,
|
||||
importError: xhr
|
||||
}))
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CANCEL_LOOKUP_ARTIST]: function(state) {
|
||||
queue.splice(0, queue.length);
|
||||
|
||||
const items = state.items.map((item) => {
|
||||
if (item.isQueued) {
|
||||
return {
|
||||
...item,
|
||||
isQueued: false
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
isLookingUpArtist: false,
|
||||
items
|
||||
});
|
||||
},
|
||||
|
||||
[CLEAR_IMPORT_ARTIST]: function(state) {
|
||||
if (abortCurrentLookup) {
|
||||
abortCurrentLookup();
|
||||
|
||||
abortCurrentLookup = null;
|
||||
}
|
||||
|
||||
queue.splice(0, queue.length);
|
||||
|
||||
return Object.assign({}, state, defaultState);
|
||||
},
|
||||
|
||||
[SET_IMPORT_ARTIST_VALUE]: function(state, { payload }) {
|
||||
const newState = getSectionState(state, section);
|
||||
const items = newState.items;
|
||||
const index = _.findIndex(items, { id: payload.id });
|
||||
|
||||
newState.items = [...items];
|
||||
|
||||
if (index >= 0) {
|
||||
const item = items[index];
|
||||
|
||||
newState.items.splice(index, 1, { ...item, ...payload });
|
||||
} else {
|
||||
newState.items.push({ ...payload });
|
||||
}
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
|
@ -8,7 +8,6 @@ import * as albums from './albumActions';
|
|||
import * as trackFiles from './trackFileActions';
|
||||
import * as albumHistory from './albumHistoryActions';
|
||||
import * as history from './historyActions';
|
||||
import * as importArtist from './importArtistActions';
|
||||
import * as interactiveImportActions from './interactiveImportActions';
|
||||
import * as oAuth from './oAuthActions';
|
||||
import * as organizePreview from './organizePreviewActions';
|
||||
|
@ -17,7 +16,6 @@ import * as paths from './pathActions';
|
|||
import * as providerOptions from './providerOptionActions';
|
||||
import * as queue from './queueActions';
|
||||
import * as releases from './releaseActions';
|
||||
import * as rootFolders from './rootFolderActions';
|
||||
import * as albumStudio from './albumStudioActions';
|
||||
import * as artist from './artistActions';
|
||||
import * as artistEditor from './artistEditorActions';
|
||||
|
@ -41,7 +39,6 @@ export default [
|
|||
trackFiles,
|
||||
albumHistory,
|
||||
history,
|
||||
importArtist,
|
||||
interactiveImportActions,
|
||||
oAuth,
|
||||
organizePreview,
|
||||
|
@ -50,7 +47,6 @@ export default [
|
|||
providerOptions,
|
||||
queue,
|
||||
releases,
|
||||
rootFolders,
|
||||
albumStudio,
|
||||
artist,
|
||||
artistEditor,
|
||||
|
|
|
@ -34,10 +34,10 @@ export const defaultState = {
|
|||
recentFolders: [],
|
||||
importMode: 'move',
|
||||
sortPredicates: {
|
||||
relativePath: function(item, direction) {
|
||||
const relativePath = item.relativePath;
|
||||
path: function(item, direction) {
|
||||
const path = item.path;
|
||||
|
||||
return relativePath.toLowerCase();
|
||||
return path.toLowerCase();
|
||||
},
|
||||
|
||||
artist: function(item, direction) {
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import { set, updateItem } from './baseActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'rootFolders';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_ROOT_FOLDERS = 'rootFolders/fetchRootFolders';
|
||||
export const ADD_ROOT_FOLDER = 'rootFolders/addRootFolder';
|
||||
export const DELETE_ROOT_FOLDER = 'rootFolders/deleteRootFolder';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchRootFolders = createThunk(FETCH_ROOT_FOLDERS);
|
||||
export const addRootFolder = createThunk(ADD_ROOT_FOLDER);
|
||||
export const deleteRootFolder = createThunk(DELETE_ROOT_FOLDER);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[FETCH_ROOT_FOLDERS]: createFetchHandler('rootFolders', '/rootFolder'),
|
||||
|
||||
[DELETE_ROOT_FOLDER]: createRemoveItemHandler(
|
||||
'rootFolders',
|
||||
'/rootFolder',
|
||||
(state) => state.rootFolders
|
||||
),
|
||||
|
||||
[ADD_ROOT_FOLDER]: function(getState, payload, dispatch) {
|
||||
const path = payload.path;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/rootFolder',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({ path }),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
updateItem({
|
||||
section,
|
||||
...data
|
||||
}),
|
||||
|
||||
set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: xhr
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({}, defaultState, section);
|
|
@ -20,6 +20,7 @@ import qualityDefinitions from './Settings/qualityDefinitions';
|
|||
import qualityProfiles from './Settings/qualityProfiles';
|
||||
import releaseProfiles from './Settings/releaseProfiles';
|
||||
import remotePathMappings from './Settings/remotePathMappings';
|
||||
import rootFolders from './Settings/rootFolders';
|
||||
import ui from './Settings/ui';
|
||||
|
||||
export * from './Settings/delayProfiles';
|
||||
|
@ -41,6 +42,7 @@ export * from './Settings/qualityDefinitions';
|
|||
export * from './Settings/qualityProfiles';
|
||||
export * from './Settings/releaseProfiles';
|
||||
export * from './Settings/remotePathMappings';
|
||||
export * from './Settings/rootFolders';
|
||||
export * from './Settings/ui';
|
||||
|
||||
//
|
||||
|
@ -73,6 +75,7 @@ export const defaultState = {
|
|||
qualityProfiles: qualityProfiles.defaultState,
|
||||
releaseProfiles: releaseProfiles.defaultState,
|
||||
remotePathMappings: remotePathMappings.defaultState,
|
||||
rootFolders: rootFolders.defaultState,
|
||||
ui: ui.defaultState
|
||||
};
|
||||
|
||||
|
@ -113,6 +116,7 @@ export const actionHandlers = handleThunks({
|
|||
...qualityProfiles.actionHandlers,
|
||||
...releaseProfiles.actionHandlers,
|
||||
...remotePathMappings.actionHandlers,
|
||||
...rootFolders.actionHandlers,
|
||||
...ui.actionHandlers
|
||||
});
|
||||
|
||||
|
@ -144,6 +148,7 @@ export const reducers = createHandleActions({
|
|||
...qualityProfiles.reducers,
|
||||
...releaseProfiles.reducers,
|
||||
...remotePathMappings.reducers,
|
||||
...rootFolders.reducers,
|
||||
...ui.reducers
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
|
@ -45,11 +45,6 @@ export const defaultState = {
|
|||
label: 'Path',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: 'Relative Path',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: 'Duration',
|
||||
|
|
|
@ -28,8 +28,8 @@ const columns = [
|
|||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: 'Relative Path',
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
|
|
@ -65,7 +65,7 @@ function createMapStateToProps() {
|
|||
const trackFile = _.find(trackFiles.items, { id: track.trackFileId });
|
||||
|
||||
return {
|
||||
relativePath: trackFile.relativePath,
|
||||
path: trackFile.path,
|
||||
quality: trackFile.quality,
|
||||
...track
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ function TrackFileEditorRow(props) {
|
|||
const {
|
||||
id,
|
||||
trackNumber,
|
||||
relativePath,
|
||||
path,
|
||||
quality,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
|
@ -29,7 +29,7 @@ function TrackFileEditorRow(props) {
|
|||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{relativePath}
|
||||
{path}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
|
@ -44,7 +44,7 @@ function TrackFileEditorRow(props) {
|
|||
TrackFileEditorRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
trackNumber: PropTypes.string.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
|
|
|
@ -69,7 +69,8 @@ class UnmappedFilesTable extends Component {
|
|||
sortDirection,
|
||||
onTableOptionChange,
|
||||
onSortPress,
|
||||
deleteUnmappedFile,
|
||||
isScanningFolders,
|
||||
onAddMissingArtistsPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
|
@ -80,6 +81,16 @@ class UnmappedFilesTable extends Component {
|
|||
return (
|
||||
<PageContent title="UnmappedFiles">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Add missing"
|
||||
iconName={icons.ADD_MISSING_ARTISTS}
|
||||
isDisabled={isPopulated && !error && !items.length}
|
||||
isSpinning={isScanningFolders}
|
||||
onPress={onAddMissingArtistsPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
|
@ -148,7 +159,9 @@ UnmappedFilesTable.propTypes = {
|
|||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
deleteUnmappedFile: PropTypes.func.isRequired
|
||||
deleteUnmappedFile: PropTypes.func.isRequired,
|
||||
isScanningFolders: PropTypes.bool.isRequired,
|
||||
onAddMissingArtistsPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default UnmappedFilesTable;
|
||||
|
|
|
@ -5,17 +5,22 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import { fetchTrackFiles, deleteTrackFile, setTrackFilesSort, setTrackFilesTableOption } from 'Store/Actions/trackFileActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import UnmappedFilesTable from './UnmappedFilesTable';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('trackFiles'),
|
||||
createCommandExecutingSelector(commandNames.RESCAN_FOLDERS),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
trackFiles,
|
||||
isScanningFolders,
|
||||
dimensionsState
|
||||
) => {
|
||||
// trackFiles could pick up mapped entries via signalR so filter again here
|
||||
|
@ -27,6 +32,7 @@ function createMapStateToProps() {
|
|||
return {
|
||||
items: unmappedFiles,
|
||||
...otherProps,
|
||||
isScanningFolders,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
}
|
||||
|
@ -49,6 +55,13 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
|
||||
deleteUnmappedFile(id) {
|
||||
dispatch(deleteTrackFile({ id }));
|
||||
},
|
||||
|
||||
onAddMissingArtistsPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.RESCAN_FOLDERS,
|
||||
filter: 'matched'
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ namespace Lidarr.Api.V1.Albums
|
|||
IMapCoversToLocal coverMapper,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
ProfileExistsValidator profileExistsValidator,
|
||||
QualityProfileExistsValidator qualityProfileExistsValidator,
|
||||
MetadataProfileExistsValidator metadataProfileExistsValidator)
|
||||
|
||||
: base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
|
||||
|
@ -54,7 +54,7 @@ namespace Lidarr.Api.V1.Albums
|
|||
Put("/monitor", x => SetAlbumsMonitored());
|
||||
|
||||
PostValidator.RuleFor(s => s.ForeignAlbumId).NotEmpty();
|
||||
PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(profileExistsValidator);
|
||||
PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(qualityProfileExistsValidator);
|
||||
PostValidator.RuleFor(s => s.Artist.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
|
||||
PostValidator.RuleFor(s => s.Artist.RootFolderPath).IsValidPath().When(s => s.Artist.Path.IsNullOrWhiteSpace());
|
||||
PostValidator.RuleFor(s => s.Artist.ForeignArtistId).NotEmpty();
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace Lidarr.Api.V1.Artist
|
|||
ArtistExistsValidator artistExistsValidator,
|
||||
ArtistAncestorValidator artistAncestorValidator,
|
||||
SystemFolderValidator systemFolderValidator,
|
||||
ProfileExistsValidator profileExistsValidator,
|
||||
QualityProfileExistsValidator qualityProfileExistsValidator,
|
||||
MetadataProfileExistsValidator metadataProfileExistsValidator)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
|
@ -85,7 +85,7 @@ namespace Lidarr.Api.V1.Artist
|
|||
.SetValidator(systemFolderValidator)
|
||||
.When(s => !s.Path.IsNullOrWhiteSpace());
|
||||
|
||||
SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(profileExistsValidator);
|
||||
SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(qualityProfileExistsValidator);
|
||||
SharedValidator.RuleFor(s => s.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
|
||||
|
||||
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
|
||||
|
|
|
@ -62,7 +62,6 @@ namespace Lidarr.Api.V1.FileSystem
|
|||
return _diskScanService.GetAudioFiles(path).Select(f => new
|
||||
{
|
||||
Path = f.FullName,
|
||||
RelativePath = path.GetRelativePath(f.FullName),
|
||||
Name = f.Name
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace Lidarr.Api.V1.ImportLists
|
|||
public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper();
|
||||
|
||||
public ImportListModule(ImportListFactory importListFactory,
|
||||
ProfileExistsValidator profileExistsValidator,
|
||||
QualityProfileExistsValidator qualityProfileExistsValidator,
|
||||
MetadataProfileExistsValidator metadataProfileExistsValidator)
|
||||
: base(importListFactory, "importlist", ResourceMapper)
|
||||
{
|
||||
|
@ -17,7 +17,7 @@ namespace Lidarr.Api.V1.ImportLists
|
|||
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.MetadataProfileId));
|
||||
|
||||
SharedValidator.RuleFor(c => c.RootFolderPath).IsValidPath();
|
||||
SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(profileExistsValidator);
|
||||
SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(qualityProfileExistsValidator);
|
||||
SharedValidator.RuleFor(c => c.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,6 @@ namespace Lidarr.Api.V1.ManualImport
|
|||
{
|
||||
Id = resource.Id,
|
||||
Path = resource.Path,
|
||||
RelativePath = resource.RelativePath,
|
||||
Name = resource.Name,
|
||||
Size = resource.Size,
|
||||
Artist = resource.Artist == null ? null : _artistService.GetArtist(resource.Artist.Id),
|
||||
|
|
|
@ -14,7 +14,6 @@ namespace Lidarr.Api.V1.ManualImport
|
|||
public class ManualImportResource : RestResource
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public string Name { get; set; }
|
||||
public long Size { get; set; }
|
||||
public ArtistResource Artist { get; set; }
|
||||
|
@ -44,7 +43,6 @@ namespace Lidarr.Api.V1.ManualImport
|
|||
{
|
||||
Id = model.Id,
|
||||
Path = model.Path,
|
||||
RelativePath = model.RelativePath,
|
||||
Name = model.Name,
|
||||
Size = model.Size,
|
||||
Artist = model.Artist.ToResource(),
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Lidarr.Http;
|
||||
using Lidarr.Http.REST;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.SignalR;
|
||||
|
||||
|
@ -18,7 +20,9 @@ namespace Lidarr.Api.V1.RootFolders
|
|||
MappedNetworkDriveValidator mappedNetworkDriveValidator,
|
||||
StartupFolderValidator startupFolderValidator,
|
||||
SystemFolderValidator systemFolderValidator,
|
||||
FolderWritableValidator folderWritableValidator)
|
||||
FolderWritableValidator folderWritableValidator,
|
||||
QualityProfileExistsValidator qualityProfileExistsValidator,
|
||||
MetadataProfileExistsValidator metadataProfileExistsValidator)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_rootFolderService = rootFolderService;
|
||||
|
@ -26,17 +30,29 @@ namespace Lidarr.Api.V1.RootFolders
|
|||
GetResourceAll = GetRootFolders;
|
||||
GetResourceById = GetRootFolder;
|
||||
CreateResource = CreateRootFolder;
|
||||
UpdateResource = UpdateRootFolder;
|
||||
DeleteResource = DeleteFolder;
|
||||
|
||||
SharedValidator.RuleFor(c => c.Path)
|
||||
.Cascade(CascadeMode.StopOnFirstFailure)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderValidator)
|
||||
.SetValidator(mappedNetworkDriveValidator)
|
||||
.SetValidator(startupFolderValidator)
|
||||
.SetValidator(pathExistsValidator)
|
||||
.SetValidator(systemFolderValidator)
|
||||
.SetValidator(folderWritableValidator);
|
||||
.Cascade(CascadeMode.StopOnFirstFailure)
|
||||
.IsValidPath()
|
||||
.SetValidator(mappedNetworkDriveValidator)
|
||||
.SetValidator(startupFolderValidator)
|
||||
.SetValidator(pathExistsValidator)
|
||||
.SetValidator(systemFolderValidator)
|
||||
.SetValidator(folderWritableValidator);
|
||||
|
||||
PostValidator.RuleFor(c => c.Path)
|
||||
.SetValidator(rootFolderValidator);
|
||||
|
||||
SharedValidator.RuleFor(c => c.Name)
|
||||
.NotEmpty();
|
||||
|
||||
SharedValidator.RuleFor(c => c.DefaultMetadataProfileId)
|
||||
.SetValidator(metadataProfileExistsValidator);
|
||||
|
||||
SharedValidator.RuleFor(c => c.DefaultQualityProfileId)
|
||||
.SetValidator(qualityProfileExistsValidator);
|
||||
}
|
||||
|
||||
private RootFolderResource GetRootFolder(int id)
|
||||
|
@ -51,9 +67,21 @@ namespace Lidarr.Api.V1.RootFolders
|
|||
return _rootFolderService.Add(model).Id;
|
||||
}
|
||||
|
||||
private void UpdateRootFolder(RootFolderResource rootFolderResource)
|
||||
{
|
||||
var model = rootFolderResource.ToModel();
|
||||
|
||||
if (model.Path != rootFolderResource.Path)
|
||||
{
|
||||
throw new BadRequestException("Cannot edit root folder path");
|
||||
}
|
||||
|
||||
_rootFolderService.Update(model);
|
||||
}
|
||||
|
||||
private List<RootFolderResource> GetRootFolders()
|
||||
{
|
||||
return _rootFolderService.AllWithUnmappedFolders().ToResource();
|
||||
return _rootFolderService.AllWithSpaceStats().ToResource();
|
||||
}
|
||||
|
||||
private void DeleteFolder(int id)
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Lidarr.Http.REST;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
|
||||
namespace Lidarr.Api.V1.RootFolders
|
||||
{
|
||||
public class RootFolderResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Path { get; set; }
|
||||
public int DefaultMetadataProfileId { get; set; }
|
||||
public int DefaultQualityProfileId { get; set; }
|
||||
public MonitorTypes DefaultMonitorOption { get; set; }
|
||||
public HashSet<int> DefaultTags { get; set; }
|
||||
|
||||
public bool Accessible { get; set; }
|
||||
public long? FreeSpace { get; set; }
|
||||
public long? TotalSpace { get; set; }
|
||||
|
||||
public List<UnmappedFolder> UnmappedFolders { get; set; }
|
||||
}
|
||||
|
||||
public static class RootFolderResourceMapper
|
||||
|
@ -28,11 +33,16 @@ namespace Lidarr.Api.V1.RootFolders
|
|||
{
|
||||
Id = model.Id,
|
||||
|
||||
Name = model.Name,
|
||||
Path = model.Path,
|
||||
DefaultMetadataProfileId = model.DefaultMetadataProfileId,
|
||||
DefaultQualityProfileId = model.DefaultQualityProfileId,
|
||||
DefaultMonitorOption = model.DefaultMonitorOption,
|
||||
DefaultTags = model.DefaultTags,
|
||||
|
||||
Accessible = model.Accessible,
|
||||
FreeSpace = model.FreeSpace,
|
||||
TotalSpace = model.TotalSpace,
|
||||
UnmappedFolders = model.UnmappedFolders
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -46,12 +56,13 @@ namespace Lidarr.Api.V1.RootFolders
|
|||
return new RootFolder
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Name = resource.Name,
|
||||
Path = resource.Path,
|
||||
|
||||
//Accessible
|
||||
//FreeSpace
|
||||
//UnmappedFolders
|
||||
DefaultMetadataProfileId = resource.DefaultMetadataProfileId,
|
||||
DefaultQualityProfileId = resource.DefaultQualityProfileId,
|
||||
DefaultMonitorOption = resource.DefaultMonitorOption,
|
||||
DefaultTags = resource.DefaultTags
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ namespace Lidarr.Api.V1.TrackFiles
|
|||
{
|
||||
public int ArtistId { get; set; }
|
||||
public int AlbumId { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public string Path { get; set; }
|
||||
public long Size { get; set; }
|
||||
public DateTime DateAdded { get; set; }
|
||||
|
@ -74,7 +73,6 @@ namespace Lidarr.Api.V1.TrackFiles
|
|||
ArtistId = artist.Id,
|
||||
AlbumId = model.AlbumId,
|
||||
Path = model.Path,
|
||||
RelativePath = artist.Path.GetRelativePath(model.Path),
|
||||
Size = model.Size,
|
||||
DateAdded = model.DateAdded,
|
||||
Quality = model.Quality,
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace Lidarr.Api.V1.Tracks
|
|||
public int AlbumId { get; set; }
|
||||
public List<int> TrackNumbers { get; set; }
|
||||
public int TrackFileId { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public string Path { get; set; }
|
||||
public List<TagDifference> Changes { get; set; }
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ namespace Lidarr.Api.V1.Tracks
|
|||
AlbumId = model.AlbumId,
|
||||
TrackNumbers = model.TrackNumbers.ToList(),
|
||||
TrackFileId = model.TrackFileId,
|
||||
RelativePath = model.RelativePath,
|
||||
Path = model.Path,
|
||||
Changes = model.Changes.Select(x => new TagDifference
|
||||
{
|
||||
Field = x.Key,
|
||||
|
|
|
@ -153,5 +153,15 @@ namespace NzbDrone.Common.Extensions
|
|||
{
|
||||
return string.Join(separator, source.Select(predicate));
|
||||
}
|
||||
|
||||
public static TSource MostCommon<TSource>(this IEnumerable<TSource> items)
|
||||
{
|
||||
return items.GroupBy(x => x).OrderByDescending(x => x.Count()).First().Key;
|
||||
}
|
||||
|
||||
public static TResult MostCommon<TSource, TResult>(this IEnumerable<TSource> items, Func<TSource, TResult> predicate)
|
||||
{
|
||||
return items.Select(predicate).GroupBy(x => x).OrderByDescending(x => x.Count()).First().Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
|
@ -6,6 +7,7 @@ using NUnit.Framework;
|
|||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.DiskSpace;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
|
@ -14,14 +16,20 @@ namespace NzbDrone.Core.Test.DiskSpace
|
|||
[TestFixture]
|
||||
public class DiskSpaceServiceFixture : CoreTest<DiskSpaceService>
|
||||
{
|
||||
private string _artistFolder;
|
||||
private string _artostFolder2;
|
||||
private RootFolder _rootDir;
|
||||
private string _artistFolder1;
|
||||
private string _artistFolder2;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_artistFolder = @"G:\fasdlfsdf\artist".AsOsAgnostic();
|
||||
_artostFolder2 = @"G:\fasdlfsdf\artist2".AsOsAgnostic();
|
||||
_rootDir = new RootFolder { Path = @"G:\fasdlfsdf".AsOsAgnostic() };
|
||||
_artistFolder1 = Path.Combine(_rootDir.Path, "artist1");
|
||||
_artistFolder2 = Path.Combine(_rootDir.Path, "artist2");
|
||||
|
||||
Mocker.GetMock<IRootFolderService>()
|
||||
.Setup(x => x.All())
|
||||
.Returns(new List<RootFolder>() { _rootDir });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.GetMounts())
|
||||
|
@ -59,9 +67,9 @@ namespace NzbDrone.Core.Test.DiskSpace
|
|||
[Test]
|
||||
public void should_check_diskspace_for_artist_folders()
|
||||
{
|
||||
GivenArtist(new Artist { Path = _artistFolder });
|
||||
GivenArtist(new Artist { Path = _artistFolder1 });
|
||||
|
||||
GivenExistingFolder(_artistFolder);
|
||||
GivenExistingFolder(_artistFolder1);
|
||||
|
||||
var freeSpace = Subject.GetFreeSpace();
|
||||
|
||||
|
@ -71,10 +79,10 @@ namespace NzbDrone.Core.Test.DiskSpace
|
|||
[Test]
|
||||
public void should_check_diskspace_for_same_root_folder_only_once()
|
||||
{
|
||||
GivenArtist(new Artist { Path = _artistFolder }, new Artist { Path = _artostFolder2 });
|
||||
GivenArtist(new Artist { Path = _artistFolder1 }, new Artist { Path = _artistFolder2 });
|
||||
|
||||
GivenExistingFolder(_artistFolder);
|
||||
GivenExistingFolder(_artostFolder2);
|
||||
GivenExistingFolder(_artistFolder1);
|
||||
GivenExistingFolder(_artistFolder2);
|
||||
|
||||
var freeSpace = Subject.GetFreeSpace();
|
||||
|
||||
|
@ -84,19 +92,6 @@ namespace NzbDrone.Core.Test.DiskSpace
|
|||
.Verify(v => v.GetAvailableSpace(It.IsAny<string>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_check_diskspace_for_missing_artist_folders()
|
||||
{
|
||||
GivenArtist(new Artist { Path = _artistFolder });
|
||||
|
||||
var freeSpace = Subject.GetFreeSpace();
|
||||
|
||||
freeSpace.Should().BeEmpty();
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Verify(v => v.GetAvailableSpace(It.IsAny<string>()), Times.Never());
|
||||
}
|
||||
|
||||
[TestCase("/boot")]
|
||||
[TestCase("/var/lib/rancher")]
|
||||
[TestCase("/var/lib/rancher/volumes")]
|
||||
|
@ -114,6 +109,10 @@ namespace NzbDrone.Core.Test.DiskSpace
|
|||
.Setup(v => v.GetMounts())
|
||||
.Returns(new List<IMount> { mount.Object });
|
||||
|
||||
Mocker.GetMock<IRootFolderService>()
|
||||
.Setup(x => x.All())
|
||||
.Returns(new List<RootFolder>());
|
||||
|
||||
var freeSpace = Subject.GetFreeSpace();
|
||||
|
||||
freeSpace.Should().BeEmpty();
|
||||
|
|
|
@ -9,7 +9,6 @@ using FluentAssertions;
|
|||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.TrackImport;
|
||||
|
@ -41,11 +40,15 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Build();
|
||||
|
||||
Mocker.GetMock<IRootFolderService>()
|
||||
.Setup(s => s.GetBestRootFolderPath(It.IsAny<string>()))
|
||||
.Returns(_rootFolder);
|
||||
.Setup(s => s.GetBestRootFolder(It.IsAny<string>()))
|
||||
.Returns(new RootFolder { Path = _rootFolder });
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Setup(s => s.GetArtists(It.IsAny<List<int>>()))
|
||||
.Returns(new List<Artist>());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<FilterFilesType>(), It.IsAny<bool>()))
|
||||
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
|
||||
.Returns(new List<ImportDecision<LocalTrack>>());
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
|
@ -57,8 +60,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Returns(new List<TrackFile>());
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(v => v.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<FilterFilesType>()))
|
||||
.Returns((List<IFileInfo> files, Artist artist, FilterFilesType filter) => files);
|
||||
.Setup(v => v.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<FilterFilesType>()))
|
||||
.Returns((List<IFileInfo> files, FilterFilesType filter) => files);
|
||||
}
|
||||
|
||||
private void GivenRootFolder(params string[] subfolders)
|
||||
|
@ -112,7 +115,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
[Test]
|
||||
public void should_not_scan_if_root_folder_does_not_exist()
|
||||
{
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
|
||||
|
@ -120,15 +123,18 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Verify(v => v.FolderExists(_artist.Path), Times.Never());
|
||||
|
||||
Mocker.GetMock<IMediaFileTableCleanupService>()
|
||||
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Never());
|
||||
.Verify(v => v.Clean(It.IsAny<string>(), It.IsAny<List<string>>()), Times.Never());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_scan_if_artist_root_folder_is_empty()
|
||||
public void should_not_scan_if_root_folder_is_empty()
|
||||
{
|
||||
GivenRootFolder();
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
|
||||
|
@ -136,72 +142,23 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Verify(v => v.FolderExists(_artist.Path), Times.Never());
|
||||
|
||||
Mocker.GetMock<IMediaFileTableCleanupService>()
|
||||
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Never());
|
||||
.Verify(v => v.Clean(It.IsAny<string>(), It.IsAny<List<string>>()), Times.Never());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), _artist, FilterFilesType.Known, true), Times.Never());
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_create_if_artist_folder_does_not_exist_but_create_folder_enabled()
|
||||
public void should_clean_if_folder_does_not_exist()
|
||||
{
|
||||
GivenRootFolder(_otherArtistFolder);
|
||||
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.CreateEmptyArtistFolders)
|
||||
.Returns(true);
|
||||
|
||||
Subject.Scan(_artist);
|
||||
|
||||
DiskProvider.FolderExists(_artist.Path).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_create_if_artist_folder_does_not_exist_and_create_folder_disabled()
|
||||
{
|
||||
GivenRootFolder(_otherArtistFolder);
|
||||
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.CreateEmptyArtistFolders)
|
||||
.Returns(false);
|
||||
|
||||
Subject.Scan(_artist);
|
||||
|
||||
DiskProvider.FolderExists(_artist.Path).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_clean_but_not_import_if_artist_folder_does_not_exist()
|
||||
{
|
||||
GivenRootFolder(_otherArtistFolder);
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
DiskProvider.FolderExists(_artist.Path).Should().BeFalse();
|
||||
|
||||
Mocker.GetMock<IMediaFileTableCleanupService>()
|
||||
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Once());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), _artist, FilterFilesType.Known, true), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_clean_but_not_import_if_artist_folder_does_not_exist_and_create_folder_enabled()
|
||||
{
|
||||
GivenRootFolder(_otherArtistFolder);
|
||||
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.CreateEmptyArtistFolders)
|
||||
.Returns(true);
|
||||
|
||||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMediaFileTableCleanupService>()
|
||||
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Once());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), _artist, FilterFilesType.Known, true), Times.Never());
|
||||
.Verify(v => v.Clean(It.IsAny<string>(), It.IsAny<List<string>>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -215,10 +172,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "s01e01.flac")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 2), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 2), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -235,10 +192,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -253,10 +210,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -276,10 +233,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "Season 2", "s02e02.flac"),
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 4), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 4), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -292,10 +249,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "Album 1", ".t01.mp3")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -311,10 +268,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -331,10 +288,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -348,10 +305,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -365,10 +322,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -384,10 +341,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 2), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 2), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -402,20 +359,20 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Path.Combine(_artist.Path, "24 The Status Quo Combustion.flac")
|
||||
});
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
|
||||
}
|
||||
|
||||
private void GivenRejections()
|
||||
{
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Setup(x => x.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<FilterFilesType>(), It.IsAny<bool>()))
|
||||
.Returns((List<IFileInfo> fileList, Artist artist, FilterFilesType filter, bool includeExisting) =>
|
||||
.Setup(x => x.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
|
||||
.Returns((List<IFileInfo> fileList, IdentificationOverrides idOverrides, ImportDecisionMakerInfo idInfo, ImportDecisionMakerConfig idConfig) =>
|
||||
fileList.Select(x => new LocalTrack
|
||||
{
|
||||
Artist = artist,
|
||||
Artist = _artist,
|
||||
Path = x.FullName,
|
||||
Modified = x.LastWriteTimeUtc,
|
||||
FileTrackInfo = new ParsedTrackInfo()
|
||||
|
@ -437,7 +394,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
GivenKnownFiles(new List<string>());
|
||||
GivenRejections();
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Verify(x => x.AddMany(It.Is<List<TrackFile>>(l => l.Select(t => t.Path).SequenceEqual(files))),
|
||||
|
@ -457,7 +414,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
GivenKnownFiles(files.GetRange(1, 1));
|
||||
GivenRejections();
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Verify(x => x.AddMany(It.Is<List<TrackFile>>(l => l.Select(t => t.Path).SequenceEqual(files.GetRange(0, 1)))),
|
||||
|
@ -477,7 +434,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
GivenKnownFiles(files);
|
||||
GivenRejections();
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Verify(x => x.AddMany(It.Is<List<TrackFile>>(l => l.Count == 0)),
|
||||
|
@ -501,7 +458,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
GivenKnownFiles(files);
|
||||
GivenRejections();
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Verify(x => x.Update(It.Is<List<TrackFile>>(l => l.Count == 0)),
|
||||
|
@ -525,7 +482,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
GivenKnownFiles(files);
|
||||
GivenRejections();
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Verify(x => x.Update(It.Is<List<TrackFile>>(l => l.Count == 2)),
|
||||
|
@ -556,14 +513,14 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Build();
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Setup(x => x.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<FilterFilesType>(), It.IsAny<bool>()))
|
||||
.Setup(x => x.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
|
||||
.Returns(new List<ImportDecision<LocalTrack>> { new ImportDecision<LocalTrack>(localTrack, new Rejection("Reject")) });
|
||||
|
||||
Subject.Scan(_artist);
|
||||
Subject.Scan(new List<string> { _artist.Path });
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Verify(x => x.Update(It.Is<List<TrackFile>>(
|
||||
l => l.Count == 1 &&
|
||||
l => l.Count == 1 &&
|
||||
l[0].Path == localTrack.Path &&
|
||||
l[0].Modified == localTrack.Modified &&
|
||||
l[0].Size == localTrack.Size &&
|
||||
|
|
|
@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||
imported.Add(new ImportDecision<LocalTrack>(localTrack));
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Setup(s => s.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), null))
|
||||
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
|
||||
.Returns(imported);
|
||||
|
||||
Mocker.GetMock<IImportApprovedTracks>()
|
||||
|
@ -130,8 +130,8 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||
Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory));
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(c => c.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedTrackInfo>()),
|
||||
Times.Never());
|
||||
.Verify(c => c.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()),
|
||||
Times.Never());
|
||||
|
||||
VerifyNoImport();
|
||||
}
|
||||
|
@ -181,7 +181,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||
imported.Add(new ImportDecision<LocalTrack>(localTrack));
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Setup(s => s.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), null))
|
||||
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
|
||||
.Returns(imported);
|
||||
|
||||
Mocker.GetMock<IImportApprovedTracks>()
|
||||
|
@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||
imported.Add(new ImportDecision<LocalTrack>(localTrack));
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Setup(s => s.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), null))
|
||||
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
|
||||
.Returns(imported);
|
||||
|
||||
Mocker.GetMock<IImportApprovedTracks>()
|
||||
|
@ -278,7 +278,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||
imported.Add(new ImportDecision<LocalTrack>(localTrack));
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Setup(s => s.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), null))
|
||||
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
|
||||
.Returns(imported);
|
||||
|
||||
Mocker.GetMock<IImportApprovedTracks>()
|
||||
|
|
|
@ -123,7 +123,6 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||
{
|
||||
VerifyData();
|
||||
var firstReleaseFiles = Subject.GetFilesWithBasePath(dir.AsOsAgnostic());
|
||||
VerifyEagerLoaded(firstReleaseFiles);
|
||||
|
||||
firstReleaseFiles.Should().HaveCount(2);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue