diff --git a/frontend/src/Artist/Details/AlbumRowConnector.js b/frontend/src/Artist/Details/AlbumRowConnector.js
index 6e92fb1d4..f00bd3fce 100644
--- a/frontend/src/Artist/Details/AlbumRowConnector.js
+++ b/frontend/src/Artist/Details/AlbumRowConnector.js
@@ -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
};
}
);
diff --git a/frontend/src/Artist/Editor/ArtistEditorConnector.js b/frontend/src/Artist/Editor/ArtistEditorConnector.js
index c0188ee6d..61f276f41 100644
--- a/frontend/src/Artist/Editor/ArtistEditorConnector.js
+++ b/frontend/src/Artist/Editor/ArtistEditorConnector.js
@@ -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';
diff --git a/frontend/src/Artist/Editor/ArtistEditorFooter.js b/frontend/src/Artist/Editor/ArtistEditorFooter.js
index ccf044c53..59bdcd056 100644
--- a/frontend/src/Artist/Editor/ArtistEditorFooter.js
+++ b/frontend/src/Artist/Editor/ArtistEditorFooter.js
@@ -217,6 +217,7 @@ class ArtistEditorFooter extends Component {
name="metadataProfileId"
value={metadataProfileId}
includeNoChange={true}
+ includeNone={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
diff --git a/frontend/src/Artist/Index/ArtistIndex.js b/frontend/src/Artist/Index/ArtistIndex.js
index 1fa9388b0..e250d61f5 100644
--- a/frontend/src/Artist/Index/ArtistIndex.js
+++ b/frontend/src/Artist/Index/ArtistIndex.js
@@ -217,7 +217,6 @@ class ArtistIndex extends Component {
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isRefreshingArtist}
- isDisabled={hasNoArtist}
onPress={onRefreshArtistPress}
/>
diff --git a/frontend/src/Artist/NoArtist.js b/frontend/src/Artist/NoArtist.js
index 1db9f41db..76c2336bc 100644
--- a/frontend/src/Artist/NoArtist.js
+++ b/frontend/src/Artist/NoArtist.js
@@ -20,15 +20,15 @@ function NoArtist(props) {
return (
- 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.
- Import Existing Artist(s)
+ Add Root Folder
diff --git a/frontend/src/Commands/commandNames.js b/frontend/src/Commands/commandNames.js
index 110f94939..b3ea2f94e 100644
--- a/frontend/src/Commands/commandNames.js
+++ b/frontend/src/Commands/commandNames.js
@@ -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';
diff --git a/frontend/src/Components/Form/RootFolderSelectInput.js b/frontend/src/Components/Form/RootFolderSelectInput.js
index 08d88e5f1..e585bd524 100644
--- a/frontend/src/Components/Form/RootFolderSelectInput.js
+++ b/frontend/src/Components/Form/RootFolderSelectInput.js
@@ -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 {
-
);
@@ -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;
diff --git a/frontend/src/Components/Form/RootFolderSelectInputConnector.js b/frontend/src/Components/Form/RootFolderSelectInputConnector.js
index b76501dc1..928ecea28 100644
--- a/frontend/src/Components/Form/RootFolderSelectInputConnector.js
+++ b/frontend/src/Components/Form/RootFolderSelectInputConnector.js
@@ -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);
diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.js b/frontend/src/Components/Form/RootFolderSelectInputOption.js
index a4db9cd82..0e4de1c1f 100644
--- a/frontend/src/Components/Form/RootFolderSelectInputOption.js
+++ b/frontend/src/Components/Form/RootFolderSelectInputOption.js
@@ -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 (
- {value}
+ {text}
{
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
diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js
index ffd769254..0af2f61ae 100644
--- a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js
+++ b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js
@@ -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 (
- {value}
+ {text}
{
@@ -32,6 +35,7 @@ function RootFolderSelectInputSelectedValue(props) {
}
RootFolderSelectInputSelectedValue.propTypes = {
+ name: PropTypes.string,
value: PropTypes.string,
freeSpace: PropTypes.number,
includeFreeSpace: PropTypes.bool.isRequired
diff --git a/frontend/src/Components/Form/SelectInput.js b/frontend/src/Components/Form/SelectInput.js
index 113d50a09..25899f630 100644
--- a/frontend/src/Components/Form/SelectInput.js
+++ b/frontend/src/Components/Form/SelectInput.js
@@ -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,
diff --git a/frontend/src/Components/Page/Sidebar/Messages/Message.js b/frontend/src/Components/Page/Sidebar/Messages/Message.js
index bb7a027fa..f9f95cfa5 100644
--- a/frontend/src/Components/Page/Sidebar/Messages/Message.js
+++ b/frontend/src/Components/Page/Sidebar/Messages/Message.js
@@ -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':
diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js
index a9e9491b0..cb27587df 100644
--- a/frontend/src/Components/Page/Sidebar/PageSidebar.js
+++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js
@@ -28,10 +28,6 @@ const links = [
title: 'Add New',
to: '/add/search'
},
- {
- title: 'Import',
- to: '/add/import'
- },
{
title: 'Mass Editor',
to: '/artisteditor'
diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js
index 2c0fc8f44..31779e9d1 100644
--- a/frontend/src/Components/SignalRConnector.js
+++ b/frontend/src/Components/SignalRConnector.js
@@ -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) => {
diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js
index 86ea9c58b..6643cbc73 100644
--- a/frontend/src/Helpers/Props/icons.js
+++ b/frontend/src/Helpers/Props/icons.js
@@ -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;
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js
index 1edac2b7c..c84fd567d 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js
@@ -30,8 +30,8 @@ import styles from './InteractiveImportModalContent.css';
const columns = [
{
- name: 'relativePath',
- label: 'Relative Path',
+ name: 'path',
+ label: 'Path',
isSortable: true,
isVisible: true
},
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css
index 8510d0649..2a23e6d4d 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css
@@ -1,4 +1,4 @@
-.relativePath {
+.path {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
index 06c2aed2a..0be12c0eb 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
@@ -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 = (
- {relativePath}
+ {path}
);
@@ -213,8 +213,8 @@ class InteractiveImportRow extends Component {
/>
{pathCell}
@@ -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,
diff --git a/frontend/src/Organize/OrganizePreviewModalContent.js b/frontend/src/Organize/OrganizePreviewModalContent.js
index 6f20a9d3c..c263208d4 100644
--- a/frontend/src/Organize/OrganizePreviewModalContent.js
+++ b/frontend/src/Organize/OrganizePreviewModalContent.js
@@ -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 &&
-
- All paths are relative to:
-
- {path}
-
-
-
Naming pattern:
diff --git a/frontend/src/Retag/RetagPreviewModalContent.js b/frontend/src/Retag/RetagPreviewModalContent.js
index 5530d63fb..fb7a39c1b 100644
--- a/frontend/src/Retag/RetagPreviewModalContent.js
+++ b/frontend/src/Retag/RetagPreviewModalContent.js
@@ -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 &&
-
- All paths are relative to:
-
- {path}
-
-
MusicBrainz identifiers will also be added to the files; these are not shown below.
@@ -130,7 +123,7 @@ class RetagPreviewModalContent extends Component {
-
- {
- isUnavailable ?
-
- {path}
-
-
- Unavailable
-
-
:
-
-
- {path}
-
- }
-
-
-
- {(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
-
-
-
- {isUnavailable ? '-' : unmappedFolders.length}
-
-
-
-
-
-
- );
-}
-
-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;
diff --git a/frontend/src/RootFolder/RootFolderRowConnector.js b/frontend/src/RootFolder/RootFolderRowConnector.js
deleted file mode 100644
index ab0848e87..000000000
--- a/frontend/src/RootFolder/RootFolderRowConnector.js
+++ /dev/null
@@ -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);
diff --git a/frontend/src/RootFolder/RootFolders.js b/frontend/src/RootFolder/RootFolders.js
deleted file mode 100644
index a07209ecc..000000000
--- a/frontend/src/RootFolder/RootFolders.js
+++ /dev/null
@@ -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 (
-
- );
- }
-
- if (!isFetching && !!error) {
- return (
- Unable to load root folders
- );
- }
-
- return (
-
-
- {
- items.map((rootFolder) => {
- return (
-
- );
- })
- }
-
-
- );
-}
-
-RootFolders.propTypes = {
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- error: PropTypes.object,
- items: PropTypes.arrayOf(PropTypes.object).isRequired
-};
-
-export default RootFolders;
diff --git a/frontend/src/RootFolder/RootFoldersConnector.js b/frontend/src/RootFolder/RootFoldersConnector.js
deleted file mode 100644
index 39f140bcc..000000000
--- a/frontend/src/RootFolder/RootFoldersConnector.js
+++ /dev/null
@@ -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 (
-
- );
- }
-}
-
-RootFoldersConnector.propTypes = {
- dispatchFetchRootFolders: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, mapDispatchToProps)(RootFoldersConnector);
diff --git a/frontend/src/Search/AddNewItemConnector.js b/frontend/src/Search/AddNewItemConnector.js
index ea9961ad9..cd61dcdd0 100644
--- a/frontend/src/Search/AddNewItemConnector.js
+++ b/frontend/src/Search/AddNewItemConnector.js
@@ -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() {
diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js
index 5aa8411b0..fa65813e1 100644
--- a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js
+++ b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js
@@ -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() {
diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js
index ab36f00d1..a6fe21fcf 100644
--- a/frontend/src/Settings/MediaManagement/MediaManagement.js
+++ b/frontend/src/Settings/MediaManagement/MediaManagement.js
@@ -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 {
/>
+
{
@@ -427,11 +427,6 @@ class MediaManagement extends Component {
}
}
-
-
-
-
-
);
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.css b/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.css
deleted file mode 100644
index 19b1880be..000000000
--- a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.addRootFolderButtonContainer {
- margin-top: 20px;
-}
-
-.importButtonIcon {
- margin-right: 8px;
-}
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.js b/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.js
deleted file mode 100644
index 3da2a55b9..000000000
--- a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.js
+++ /dev/null
@@ -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 (
-
-
-
- Add Root Folder
-
-
-
-
- );
- }
-}
-
-AddRootFolder.propTypes = {
- onNewRootFolderSelect: PropTypes.func.isRequired
-};
-
-export default AddRootFolder;
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolderConnector.js b/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolderConnector.js
deleted file mode 100644
index cd5f4c50d..000000000
--- a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolderConnector.js
+++ /dev/null
@@ -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);
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModal.js b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModal.js
new file mode 100644
index 000000000..6adc8046c
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModal.js
@@ -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 (
+
+
+
+ );
+}
+
+EditRootFolderModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default EditRootFolderModal;
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalConnector.js b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalConnector.js
new file mode 100644
index 000000000..d016df6ce
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalConnector.js
@@ -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 (
+
+ );
+ }
+}
+
+EditRootFolderModalConnector.propTypes = {
+ onModalClose: PropTypes.func.isRequired,
+ dispatchClearPendingChanges: PropTypes.func.isRequired,
+ dispatchCancelSaveRootFolder: PropTypes.func.isRequired
+};
+
+export default connect(null, createMapDispatchToProps)(EditRootFolderModalConnector);
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.css b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.css
new file mode 100644
index 000000000..23e22b6dc
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.css
@@ -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;
+}
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js
new file mode 100644
index 000000000..6b015f8cc
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js
@@ -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 (
+
+
+ {id ? 'Edit Root Folder' : 'Add Root Folder'}
+
+
+
+ {
+ isFetching &&
+
+ }
+
+ {
+ !isFetching && !!error &&
+ Unable to add a new root folder, please try again.
+ }
+
+ {
+ !isFetching && !error &&
+
+ }
+
+
+ {
+ id &&
+
+ Delete
+
+ }
+
+
+ Cancel
+
+
+
+ Save
+
+
+
+ );
+}
+
+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;
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContentConnector.js b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContentConnector.js
new file mode 100644
index 000000000..1285d2f7a
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContentConnector.js
@@ -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 (
+
+ );
+ }
+}
+
+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);
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.css b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.css
new file mode 100644
index 000000000..0506cc9c6
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.css
@@ -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;
+}
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js
new file mode 100644
index 000000000..01b0ac022
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js
@@ -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 (
+
+
+ {name}
+
+
+
+
+ {path}
+
+
+
+ {qualityProfile.name}
+
+
+
+ {metadataProfile.name}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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;
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/RootFolders.css b/frontend/src/Settings/MediaManagement/RootFolder/RootFolders.css
new file mode 100644
index 000000000..6ecc1572c
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/RootFolders.css
@@ -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;
+}
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/RootFolders.js b/frontend/src/Settings/MediaManagement/RootFolder/RootFolders.js
new file mode 100644
index 000000000..366f45e85
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/RootFolders.js
@@ -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 (
+
+
+
+ {
+ items.sort(sortByName).map((item) => {
+ const qualityProfile = qualityProfiles.find((profile) => profile.id === item.defaultQualityProfileId);
+ const metadataProfile = metadataProfiles.find((profile) => profile.id === item.defaultMetadataProfileId);
+ return (
+
+ );
+ })
+ }
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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;
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/RootFoldersConnector.js b/frontend/src/Settings/MediaManagement/RootFolder/RootFoldersConnector.js
new file mode 100644
index 000000000..af2cf091d
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/RootFolder/RootFoldersConnector.js
@@ -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 (
+
+ );
+ }
+}
+
+RootFoldersConnector.propTypes = {
+ dispatchFetchRootFolders: PropTypes.func.isRequired,
+ dispatchDeleteRootFolder: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(RootFoldersConnector);
diff --git a/frontend/src/Store/Actions/Settings/rootFolders.js b/frontend/src/Store/Actions/Settings/rootFolders.js
new file mode 100644
index 000000000..d7bcf34e4
--- /dev/null
+++ b/frontend/src/Store/Actions/Settings/rootFolders.js
@@ -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)
+ }
+};
diff --git a/frontend/src/Store/Actions/importArtistActions.js b/frontend/src/Store/Actions/importArtistActions.js
deleted file mode 100644
index b4b265a6d..000000000
--- a/frontend/src/Store/Actions/importArtistActions.js
+++ /dev/null
@@ -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);
diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js
index ccabf68f0..183bf9df3 100644
--- a/frontend/src/Store/Actions/index.js
+++ b/frontend/src/Store/Actions/index.js
@@ -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,
diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js
index 7b0607885..7662a917a 100644
--- a/frontend/src/Store/Actions/interactiveImportActions.js
+++ b/frontend/src/Store/Actions/interactiveImportActions.js
@@ -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) {
diff --git a/frontend/src/Store/Actions/rootFolderActions.js b/frontend/src/Store/Actions/rootFolderActions.js
deleted file mode 100644
index 3e3c7de8a..000000000
--- a/frontend/src/Store/Actions/rootFolderActions.js
+++ /dev/null
@@ -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);
diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js
index 2a7cfc8b9..c056fea31 100644
--- a/frontend/src/Store/Actions/settingsActions.js
+++ b/frontend/src/Store/Actions/settingsActions.js
@@ -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);
diff --git a/frontend/src/Store/Actions/trackActions.js b/frontend/src/Store/Actions/trackActions.js
index 44292271a..bc65fab5e 100644
--- a/frontend/src/Store/Actions/trackActions.js
+++ b/frontend/src/Store/Actions/trackActions.js
@@ -45,11 +45,6 @@ export const defaultState = {
label: 'Path',
isVisible: false
},
- {
- name: 'relativePath',
- label: 'Relative Path',
- isVisible: false
- },
{
name: 'duration',
label: 'Duration',
diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js
index f9d9cc282..ebc6ad892 100644
--- a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js
+++ b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js
@@ -28,8 +28,8 @@ const columns = [
isVisible: true
},
{
- name: 'relativePath',
- label: 'Relative Path',
+ name: 'path',
+ label: 'Path',
isVisible: true
},
{
diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js b/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js
index bfd90a44b..406d1a04f 100644
--- a/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js
+++ b/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js
@@ -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
};
diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorRow.js b/frontend/src/TrackFile/Editor/TrackFileEditorRow.js
index e475c115b..5c00e6858 100644
--- a/frontend/src/TrackFile/Editor/TrackFileEditorRow.js
+++ b/frontend/src/TrackFile/Editor/TrackFileEditorRow.js
@@ -10,7 +10,7 @@ function TrackFileEditorRow(props) {
const {
id,
trackNumber,
- relativePath,
+ path,
quality,
isSelected,
onSelectedChange
@@ -29,7 +29,7 @@ function TrackFileEditorRow(props) {
- {relativePath}
+ {path}
@@ -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
diff --git a/frontend/src/UnmappedFiles/UnmappedFilesTable.js b/frontend/src/UnmappedFiles/UnmappedFilesTable.js
index 94822299f..7b2bad819 100644
--- a/frontend/src/UnmappedFiles/UnmappedFilesTable.js
+++ b/frontend/src/UnmappedFiles/UnmappedFilesTable.js
@@ -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 (
+
+
+
+
{
// 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'
+ }));
}
};
}
diff --git a/src/Lidarr.Api.V1/Albums/AlbumModule.cs b/src/Lidarr.Api.V1/Albums/AlbumModule.cs
index 9fb2d9d70..889d05144 100644
--- a/src/Lidarr.Api.V1/Albums/AlbumModule.cs
+++ b/src/Lidarr.Api.V1/Albums/AlbumModule.cs
@@ -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();
diff --git a/src/Lidarr.Api.V1/Artist/ArtistModule.cs b/src/Lidarr.Api.V1/Artist/ArtistModule.cs
index 830d66262..362f155d0 100644
--- a/src/Lidarr.Api.V1/Artist/ArtistModule.cs
+++ b/src/Lidarr.Api.V1/Artist/ArtistModule.cs
@@ -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());
diff --git a/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs b/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs
index 67da221d6..58f899324 100644
--- a/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs
+++ b/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs
@@ -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
});
}
diff --git a/src/Lidarr.Api.V1/ImportLists/ImportListModule.cs b/src/Lidarr.Api.V1/ImportLists/ImportListModule.cs
index 4cb75fbae..8f3b1edb1 100644
--- a/src/Lidarr.Api.V1/ImportLists/ImportListModule.cs
+++ b/src/Lidarr.Api.V1/ImportLists/ImportListModule.cs
@@ -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);
}
diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs
index c9c7017b4..ce8b00680 100644
--- a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs
+++ b/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs
@@ -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),
diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs
index 4d9c2653a..8470ae13b 100644
--- a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs
+++ b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs
@@ -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(),
diff --git a/src/Lidarr.Api.V1/RootFolders/RootFolderModule.cs b/src/Lidarr.Api.V1/RootFolders/RootFolderModule.cs
index cba25a230..731acf561 100644
--- a/src/Lidarr.Api.V1/RootFolders/RootFolderModule.cs
+++ b/src/Lidarr.Api.V1/RootFolders/RootFolderModule.cs
@@ -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 GetRootFolders()
{
- return _rootFolderService.AllWithUnmappedFolders().ToResource();
+ return _rootFolderService.AllWithSpaceStats().ToResource();
}
private void DeleteFolder(int id)
diff --git a/src/Lidarr.Api.V1/RootFolders/RootFolderResource.cs b/src/Lidarr.Api.V1/RootFolders/RootFolderResource.cs
index 2d19ef40e..563eb4c08 100644
--- a/src/Lidarr.Api.V1/RootFolders/RootFolderResource.cs
+++ b/src/Lidarr.Api.V1/RootFolders/RootFolderResource.cs
@@ -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 DefaultTags { get; set; }
+
public bool Accessible { get; set; }
public long? FreeSpace { get; set; }
public long? TotalSpace { get; set; }
-
- public List 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
};
}
diff --git a/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs b/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs
index fb3f3523d..a445e3c84 100644
--- a/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs
+++ b/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs
@@ -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,
diff --git a/src/Lidarr.Api.V1/Tracks/RetagTrackResource.cs b/src/Lidarr.Api.V1/Tracks/RetagTrackResource.cs
index 34766eb7c..353233e82 100644
--- a/src/Lidarr.Api.V1/Tracks/RetagTrackResource.cs
+++ b/src/Lidarr.Api.V1/Tracks/RetagTrackResource.cs
@@ -17,7 +17,7 @@ namespace Lidarr.Api.V1.Tracks
public int AlbumId { get; set; }
public List TrackNumbers { get; set; }
public int TrackFileId { get; set; }
- public string RelativePath { get; set; }
+ public string Path { get; set; }
public List 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,
diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs
index d2bd23467..946669ff2 100644
--- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs
+++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs
@@ -153,5 +153,15 @@ namespace NzbDrone.Common.Extensions
{
return string.Join(separator, source.Select(predicate));
}
+
+ public static TSource MostCommon(this IEnumerable items)
+ {
+ return items.GroupBy(x => x).OrderByDescending(x => x.Count()).First().Key;
+ }
+
+ public static TResult MostCommon(this IEnumerable items, Func predicate)
+ {
+ return items.Select(predicate).GroupBy(x => x).OrderByDescending(x => x.Count()).First().Key;
+ }
}
}
diff --git a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs
index b673d0490..b17b4a4b2 100644
--- a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs
+++ b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs
@@ -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
{
- 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()
+ .Setup(x => x.All())
+ .Returns(new List() { _rootDir });
Mocker.GetMock()
.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()), 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()
- .Verify(v => v.GetAvailableSpace(It.IsAny()), 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 { mount.Object });
+ Mocker.GetMock()
+ .Setup(x => x.All())
+ .Returns(new List());
+
var freeSpace = Subject.GetFreeSpace();
freeSpace.Should().BeEmpty();
diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs
index 8ecf75845..70fecacbc 100644
--- a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs
+++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs
@@ -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()
- .Setup(s => s.GetBestRootFolderPath(It.IsAny()))
- .Returns(_rootFolder);
+ .Setup(s => s.GetBestRootFolder(It.IsAny()))
+ .Returns(new RootFolder { Path = _rootFolder });
+
+ Mocker.GetMock()
+ .Setup(s => s.GetArtists(It.IsAny>()))
+ .Returns(new List());
Mocker.GetMock()
- .Setup(v => v.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Setup(v => v.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(new List>());
Mocker.GetMock()
@@ -57,8 +60,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Returns(new List());
Mocker.GetMock()
- .Setup(v => v.FilterUnchangedFiles(It.IsAny>(), It.IsAny(), It.IsAny()))
- .Returns((List files, Artist artist, FilterFilesType filter) => files);
+ .Setup(v => v.FilterUnchangedFiles(It.IsAny>(), It.IsAny()))
+ .Returns((List 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 { _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()
- .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), Times.Never());
+ .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), Times.Never());
+
+ Mocker.GetMock()
+ .Verify(v => v.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _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()
- .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), Times.Never());
+ .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), Times.Never());
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.IsAny>(), _artist, FilterFilesType.Known, true), Times.Never());
+ .Verify(v => v.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), 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()
- .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()
- .Setup(s => s.CreateEmptyArtistFolders)
- .Returns(false);
-
- Subject.Scan(_artist);
+ Subject.Scan(new List { _artist.Path });
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);
-
- DiskProvider.FolderExists(_artist.Path).Should().BeFalse();
-
- Mocker.GetMock()
- .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), Times.Once());
-
- Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.IsAny>(), _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()
- .Setup(s => s.CreateEmptyArtistFolders)
- .Returns(true);
-
- Subject.Scan(_artist);
Mocker.GetMock()
- .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), Times.Once());
-
- Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.IsAny>(), _artist, FilterFilesType.Known, true), Times.Never());
+ .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 2), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 2), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 4), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 4), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 2), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 2), It.IsAny(), It.IsAny(), It.IsAny()), 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 { _artist.Path });
Mocker.GetMock()
- .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
+ .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once());
}
private void GivenRejections()
{
Mocker.GetMock()
- .Setup(x => x.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns((List fileList, Artist artist, FilterFilesType filter, bool includeExisting) =>
+ .Setup(x => x.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns((List 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());
GivenRejections();
- Subject.Scan(_artist);
+ Subject.Scan(new List { _artist.Path });
Mocker.GetMock()
.Verify(x => x.AddMany(It.Is>(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 { _artist.Path });
Mocker.GetMock()
.Verify(x => x.AddMany(It.Is>(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 { _artist.Path });
Mocker.GetMock()
.Verify(x => x.AddMany(It.Is>(l => l.Count == 0)),
@@ -501,7 +458,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
GivenKnownFiles(files);
GivenRejections();
- Subject.Scan(_artist);
+ Subject.Scan(new List { _artist.Path });
Mocker.GetMock()
.Verify(x => x.Update(It.Is>(l => l.Count == 0)),
@@ -525,7 +482,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
GivenKnownFiles(files);
GivenRejections();
- Subject.Scan(_artist);
+ Subject.Scan(new List { _artist.Path });
Mocker.GetMock()
.Verify(x => x.Update(It.Is>(l => l.Count == 2)),
@@ -556,14 +513,14 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Build();
Mocker.GetMock()
- .Setup(x => x.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Setup(x => x.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(new List> { new ImportDecision(localTrack, new Rejection("Reject")) });
- Subject.Scan(_artist);
+ Subject.Scan(new List { _artist.Path });
Mocker.GetMock()
.Verify(x => x.Update(It.Is>(
- l => l.Count == 1 &&
+ l => l.Count == 1 &&
l[0].Path == localTrack.Path &&
l[0].Modified == localTrack.Modified &&
l[0].Size == localTrack.Size &&
diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs
index 3b9aaa3d4..887eef837 100644
--- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs
+++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs
@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision(localTrack));
Mocker.GetMock()
- .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null))
+ .Setup(v => v.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(imported);
Mocker.GetMock()
@@ -130,8 +130,8 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory));
Mocker.GetMock()
- .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()),
- Times.Never());
+ .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()),
+ Times.Never());
VerifyNoImport();
}
@@ -181,7 +181,7 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision(localTrack));
Mocker.GetMock()
- .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null))
+ .Setup(v => v.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(imported);
Mocker.GetMock()
@@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision(localTrack));
Mocker.GetMock()
- .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null))
+ .Setup(v => v.GetImportDecisions(It.IsAny