mirror of
https://github.com/Readarr/Readarr.git
synced 2026-09-21 02:36:21 -04:00
New: Add/remove individual albums
This commit is contained in:
1 parent
6af29da4c9
commit
8a20c0fa83
128 files changed
+2796
-743
No files matched your search
@@ -1,241 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import { icons, kinds, inputTypes, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
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 CheckInput from 'Components/Form/CheckInput';
|
||||
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 Popover from 'Components/Tooltip/Popover';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent';
|
||||
import styles from './AddNewArtistModalContent.css';
|
||||
|
||||
class AddNewArtistModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
searchForMissingAlbums: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchForMissingAlbumsChange = ({ value }) => {
|
||||
this.setState({ searchForMissingAlbums: value });
|
||||
}
|
||||
|
||||
onQualityProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||
}
|
||||
|
||||
onMetadataProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'metadataProfileId', value: parseInt(value) });
|
||||
}
|
||||
|
||||
onAddArtistPress = () => {
|
||||
this.props.onAddArtistPress(this.state.searchForMissingAlbums);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
artistName,
|
||||
overview,
|
||||
images,
|
||||
isAdding,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
albumFolder,
|
||||
tags,
|
||||
showMetadataProfile,
|
||||
isSmallScreen,
|
||||
onModalClose,
|
||||
onInputChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{artistName}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
isSmallScreen ?
|
||||
null:
|
||||
<div className={styles.poster}>
|
||||
<ArtistPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
{
|
||||
overview ?
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
truncateText="…"
|
||||
line={8}
|
||||
text={overview}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Root Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
onChange={onInputChange}
|
||||
{...rootFolderPath}
|
||||
/>
|
||||
</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="monitor"
|
||||
onChange={onInputChange}
|
||||
{...monitor}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Quality Profile</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
onChange={this.onQualityProfileIdChange}
|
||||
{...qualityProfileId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
|
||||
<FormLabel>Metadata Profile</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.METADATA_PROFILE_SELECT}
|
||||
name="metadataProfileId"
|
||||
onChange={this.onMetadataProfileIdChange}
|
||||
{...metadataProfileId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Album Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="albumFolder"
|
||||
onChange={onInputChange}
|
||||
{...albumFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
onChange={onInputChange}
|
||||
{...tags}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<label className={styles.searchForMissingAlbumsLabelContainer}>
|
||||
<span className={styles.searchForMissingAlbumsLabel}>
|
||||
Start search for missing albums
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
containerClassName={styles.searchForMissingAlbumsContainer}
|
||||
className={styles.searchForMissingAlbumsInput}
|
||||
name="searchForMissingAlbums"
|
||||
value={this.state.searchForMissingAlbums}
|
||||
onChange={this.onSearchForMissingAlbumsChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.addButton}
|
||||
kind={kinds.SUCCESS}
|
||||
isSpinning={isAdding}
|
||||
onPress={this.onAddArtistPress}
|
||||
>
|
||||
Add {artistName}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewArtistModalContent.propTypes = {
|
||||
artistName: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
metadataProfileId: PropTypes.object,
|
||||
albumFolder: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onAddArtistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewArtistModalContent;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
function ArtistMetadataProfilePopoverContent() {
|
||||
return (
|
||||
<div>
|
||||
Select 'None' to only include items manually added via search
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistMetadataProfilePopoverContent;
|
||||
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setImportArtistValue, importArtist, clearImportArtist } from 'Store/Actions/importArtistActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { setAddArtistDefault } from 'Store/Actions/addArtistActions';
|
||||
import { setAddDefault } from 'Store/Actions/searchActions';
|
||||
import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape';
|
||||
import ImportArtist from './ImportArtist';
|
||||
|
||||
@@ -67,7 +67,7 @@ const mapDispatchToProps = {
|
||||
dispatchImportArtist: importArtist,
|
||||
dispatchClearImportArtist: clearImportArtist,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchSetAddArtistDefault: setAddArtistDefault
|
||||
dispatchSetAddDefault: setAddDefault
|
||||
};
|
||||
|
||||
class ImportArtistConnector extends Component {
|
||||
@@ -82,7 +82,7 @@ class ImportArtistConnector extends Component {
|
||||
defaultQualityProfileId,
|
||||
defaultMetadataProfileId,
|
||||
dispatchFetchRootFolders,
|
||||
dispatchSetAddArtistDefault
|
||||
dispatchSetAddDefault
|
||||
} = this.props;
|
||||
|
||||
if (!this.props.rootFoldersPopulated) {
|
||||
@@ -109,7 +109,7 @@ class ImportArtistConnector extends Component {
|
||||
}
|
||||
|
||||
if (setDefaults) {
|
||||
dispatchSetAddArtistDefault(setDefaultPayload);
|
||||
dispatchSetAddDefault(setDefaultPayload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ class ImportArtistConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onInputChange = (ids, name, value) => {
|
||||
this.props.dispatchSetAddArtistDefault({ [name]: value });
|
||||
this.props.dispatchSetAddDefault({ [name]: value });
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.dispatchSetImportArtistValue({
|
||||
@@ -164,7 +164,7 @@ ImportArtistConnector.propTypes = {
|
||||
dispatchImportArtist: PropTypes.func.isRequired,
|
||||
dispatchClearImportArtist: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchSetAddArtistDefault: PropTypes.func.isRequired
|
||||
dispatchSetAddDefault: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistConnector);
|
||||
@@ -0,0 +1,33 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import DeleteAlbumModalContentConnector from './DeleteAlbumModalContentConnector';
|
||||
|
||||
function DeleteAlbumModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<DeleteAlbumModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
DeleteAlbumModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DeleteAlbumModal;
|
||||
@@ -0,0 +1,12 @@
|
||||
.pathContainer {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pathIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.deleteFilesMessage {
|
||||
margin-top: 20px;
|
||||
color: $dangerColor;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './DeleteAlbumModalContent.css';
|
||||
|
||||
class DeleteAlbumModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
deleteFiles: false,
|
||||
addImportListExclusion: true
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDeleteFilesChange = ({ value }) => {
|
||||
this.setState({ deleteFiles: value });
|
||||
}
|
||||
|
||||
onAddImportListExclusionChange = ({ value }) => {
|
||||
this.setState({ addImportListExclusion: value });
|
||||
}
|
||||
|
||||
onDeleteAlbumConfirmed = () => {
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
const addImportListExclusion = this.state.addImportListExclusion;
|
||||
|
||||
this.setState({ deleteFiles: false });
|
||||
this.setState({ addImportListExclusion: false });
|
||||
this.props.onDeletePress(deleteFiles, addImportListExclusion);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
statistics,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
trackFileCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
const addImportListExclusion = this.state.addImportListExclusion;
|
||||
|
||||
const deleteFilesLabel = `Delete ${trackFileCount} Track Files`;
|
||||
const deleteFilesHelpText = 'Delete the track files';
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Delete - {title}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{deleteFilesLabel}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="deleteFiles"
|
||||
value={deleteFiles}
|
||||
helpText={deleteFilesHelpText}
|
||||
kind={kinds.DANGER}
|
||||
onChange={this.onDeleteFilesChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Add List Exclusion</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="addImportListExclusion"
|
||||
value={addImportListExclusion}
|
||||
helpText="Prevent album from being added to Lidarr by Import Lists or Artist Refresh"
|
||||
kind={kinds.DANGER}
|
||||
onChange={this.onAddImportListExclusionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
!addImportListExclusion &&
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
<div>If you don't add an import list exclusion and the artist has a metadata profile other than 'None' then this album may be re-added during the next artist refresh.</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
deleteFiles &&
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
<div>The album's files will be deleted.</div>
|
||||
|
||||
{
|
||||
!!trackFileCount &&
|
||||
<div>{trackFileCount} track files totaling {formatBytes(sizeOnDisk)}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteAlbumConfirmed}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteAlbumModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DeleteAlbumModalContent.defaultProps = {
|
||||
statistics: {
|
||||
trackFileCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default DeleteAlbumModalContent;
|
||||
@@ -0,0 +1,62 @@
|
||||
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 createAlbumSelector from 'Store/Selectors/createAlbumSelector';
|
||||
import { deleteAlbum } from 'Store/Actions/albumActions';
|
||||
import DeleteAlbumModalContent from './DeleteAlbumModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createAlbumSelector(),
|
||||
(album) => {
|
||||
return album;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
push,
|
||||
deleteAlbum
|
||||
};
|
||||
|
||||
class DeleteAlbumModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDeletePress = (deleteFiles, addImportListExclusion) => {
|
||||
this.props.deleteAlbum({
|
||||
id: this.props.albumId,
|
||||
deleteFiles,
|
||||
addImportListExclusion
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
|
||||
this.props.push(`${window.Lidarr.urlBase}/artist/${this.props.foreignArtistId}`);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DeleteAlbumModalContent
|
||||
{...this.props}
|
||||
onDeletePress={this.onDeletePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteAlbumModalContentConnector.propTypes = {
|
||||
albumId: PropTypes.number.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
push: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
deleteAlbum: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DeleteAlbumModalContentConnector);
|
||||
@@ -18,6 +18,7 @@ import AlbumCover from 'Album/AlbumCover';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
|
||||
import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
|
||||
import DeleteAlbumModal from 'Album/Delete/DeleteAlbumModal';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
@@ -88,6 +89,7 @@ class AlbumDetails extends Component {
|
||||
isInteractiveSearchModalOpen: false,
|
||||
isManageTracksOpen: false,
|
||||
isEditAlbumModalOpen: false,
|
||||
isDeleteAlbumModalOpen: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {}
|
||||
@@ -121,6 +123,17 @@ class AlbumDetails extends Component {
|
||||
this.setState({ isEditAlbumModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteAlbumPress = () => {
|
||||
this.setState({
|
||||
isEditAlbumModalOpen: false,
|
||||
isDeleteAlbumModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteAlbumModalClose = () => {
|
||||
this.setState({ isDeleteAlbumModalOpen: false });
|
||||
}
|
||||
|
||||
onManageTracksPress = () => {
|
||||
this.setState({ isManageTracksOpen: true });
|
||||
}
|
||||
@@ -208,6 +221,7 @@ class AlbumDetails extends Component {
|
||||
isArtistHistoryModalOpen,
|
||||
isInteractiveSearchModalOpen,
|
||||
isEditAlbumModalOpen,
|
||||
isDeleteAlbumModalOpen,
|
||||
isManageTracksOpen,
|
||||
allExpanded,
|
||||
allCollapsed,
|
||||
@@ -276,6 +290,12 @@ class AlbumDetails extends Component {
|
||||
onPress={this.onEditAlbumPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Delete"
|
||||
iconName={icons.DELETE}
|
||||
onPress={this.onDeleteAlbumPress}
|
||||
/>
|
||||
|
||||
</PageToolbarSection>
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<PageToolbarButton
|
||||
@@ -549,6 +569,14 @@ class AlbumDetails extends Component {
|
||||
albumId={id}
|
||||
artistId={artist.id}
|
||||
onModalClose={this.onEditAlbumModalClose}
|
||||
onDeleteArtistPress={this.onDeleteAlbumPress}
|
||||
/>
|
||||
|
||||
<DeleteAlbumModal
|
||||
isOpen={isDeleteAlbumModalOpen}
|
||||
albumId={id}
|
||||
foreignArtistId={artist.foreignArtistId}
|
||||
onModalClose={this.onDeleteAlbumModalClose}
|
||||
/>
|
||||
|
||||
</PageContentBodyConnector>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -20,6 +21,18 @@ function createMapStateToProps() {
|
||||
const isFetching = albums.isFetching || artist.isFetching;
|
||||
const isPopulated = albums.isPopulated && artist.isPopulated;
|
||||
|
||||
// if albums have been fetched, make sure requested one exists
|
||||
// otherwise don't map foreignAlbumId to trigger not found page
|
||||
if (!isFetching && isPopulated) {
|
||||
const albumIndex = _.findIndex(albums.items, { foreignAlbumId });
|
||||
if (albumIndex === -1) {
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
foreignAlbumId,
|
||||
isFetching,
|
||||
|
||||
@@ -5,7 +5,7 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector';
|
||||
import AddNewArtistConnector from 'AddArtist/AddNewArtist/AddNewArtistConnector';
|
||||
import AddNewItemConnector from 'Search/AddNewItemConnector';
|
||||
import ImportArtist from 'AddArtist/ImportArtist/ImportArtist';
|
||||
import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector';
|
||||
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
|
||||
@@ -72,8 +72,8 @@ function AppRoutes(props) {
|
||||
}
|
||||
|
||||
<Route
|
||||
path="/add/new"
|
||||
component={AddNewArtistConnector}
|
||||
path="/add/search"
|
||||
component={AddNewItemConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -611,7 +611,11 @@ class ArtistDetails extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.metadataMessage}>
|
||||
Missing Albums, Singles, or Other Types? Modify or Create a New <Link to='/settings/profiles'> Metadata Profile</Link>!
|
||||
Missing Albums, Singles, or Other Types? Modify or create a new
|
||||
<Link to='/settings/profiles'> Metadata Profile </Link>
|
||||
or manually
|
||||
<Link to={`/add/search?term=${encodeURIComponent(artistName)}`}> Search </Link>
|
||||
for new items!
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
|
||||
@@ -28,13 +28,15 @@ const selectAlbums = createSelector(
|
||||
|
||||
const hasAlbums = !!items.length;
|
||||
const hasMonitoredAlbums = items.some((e) => e.monitored);
|
||||
const albumTypes = _.uniq(_.map(items, 'albumType'));
|
||||
|
||||
return {
|
||||
isAlbumsFetching: isFetching,
|
||||
isAlbumsPopulated: isPopulated,
|
||||
albumsError: error,
|
||||
hasAlbums,
|
||||
hasMonitoredAlbums
|
||||
hasMonitoredAlbums,
|
||||
albumTypes
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -65,20 +67,12 @@ function createMapStateToProps() {
|
||||
(state, { foreignArtistId }) => foreignArtistId,
|
||||
selectAlbums,
|
||||
selectTrackFiles,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
createAllArtistSelector(),
|
||||
createCommandsSelector(),
|
||||
(foreignArtistId, albums, trackFiles, metadataProfiles, allArtists, commands) => {
|
||||
(foreignArtistId, albums, trackFiles, allArtists, commands) => {
|
||||
const sortedArtist = _.orderBy(allArtists, 'sortName');
|
||||
const artistIndex = _.findIndex(sortedArtist, { foreignArtistId });
|
||||
const artist = sortedArtist[artistIndex];
|
||||
const metadataProfile = _.find(metadataProfiles.items, { id: artist.metadataProfileId });
|
||||
const albumTypes = _.reduce(metadataProfile.primaryAlbumTypes, (acc, primaryType) => {
|
||||
if (primaryType.allowed) {
|
||||
acc.push(primaryType.albumType.name);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (!artist) {
|
||||
return {};
|
||||
@@ -89,7 +83,8 @@ function createMapStateToProps() {
|
||||
isAlbumsPopulated,
|
||||
albumsError,
|
||||
hasAlbums,
|
||||
hasMonitoredAlbums
|
||||
hasMonitoredAlbums,
|
||||
albumTypes
|
||||
} = albums;
|
||||
|
||||
const {
|
||||
|
||||
@@ -3,3 +3,7 @@
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
@@ -11,7 +11,10 @@ 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 Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import MoveArtistModal from 'Artist/MoveArtist/MoveArtistModal';
|
||||
import ArtistMetadataProfilePopoverContent from 'AddArtist/ArtistMetadataProfilePopoverContent';
|
||||
import styles from './EditArtistModalContent.css';
|
||||
|
||||
class EditArtistModalContent extends Component {
|
||||
@@ -122,12 +125,28 @@ class EditArtistModalContent extends Component {
|
||||
{
|
||||
showMetadataProfile &&
|
||||
<FormGroup>
|
||||
<FormLabel>Metadata Profile</FormLabel>
|
||||
<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="metadataProfileId"
|
||||
helpText="Changes will take place on next artist refresh"
|
||||
includeNone={true}
|
||||
{...metadataProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -20,7 +20,7 @@ function NoArtist(props) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
No artist found, to get started you'll want to add a new artist or import some existing ones.
|
||||
No artist found, to get started you'll want to add a new artist or album or import some existing ones.
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
@@ -34,7 +34,7 @@ function NoArtist(props) {
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
to="/add/new"
|
||||
to="/add/search"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
Add New Artist
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { metadataProfileNames } from 'Helpers/Props';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -11,14 +12,26 @@ function createMapStateToProps() {
|
||||
(state) => state.settings.metadataProfiles,
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(metadataProfiles, includeNoChange, includeMixed) => {
|
||||
const values = _.map(metadataProfiles.items.sort(sortByName), (metadataProfile) => {
|
||||
(state, { includeNone }) => includeNone,
|
||||
(metadataProfiles, includeNoChange, includeMixed, includeNone) => {
|
||||
|
||||
const profiles = metadataProfiles.items.filter((item) => item.name !== metadataProfileNames.NONE);
|
||||
const noneProfile = metadataProfiles.items.find((item) => item.name === metadataProfileNames.NONE);
|
||||
|
||||
const values = _.map(profiles.sort(sortByName), (metadataProfile) => {
|
||||
return {
|
||||
key: metadataProfile.id,
|
||||
value: metadataProfile.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeNone) {
|
||||
values.push({
|
||||
key: noneProfile.id,
|
||||
value: noneProfile.name
|
||||
});
|
||||
}
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
@@ -88,6 +101,7 @@ MetadataProfileSelectInputConnector.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
includeNone: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ class ArtistSearchInput extends Component {
|
||||
}
|
||||
|
||||
suggestionGroups.push({
|
||||
title: 'Add New Artist',
|
||||
title: 'Add New Item',
|
||||
suggestions: [
|
||||
{
|
||||
type: ADD_NEW_TYPE,
|
||||
|
||||
@@ -58,7 +58,7 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
},
|
||||
|
||||
onGoToAddNewArtist(query) {
|
||||
dispatch(push(`${window.Lidarr.urlBase}/add/new?term=${encodeURIComponent(query)}`));
|
||||
dispatch(push(`${window.Lidarr.urlBase}/add/search?term=${encodeURIComponent(query)}`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const links = [
|
||||
children: [
|
||||
{
|
||||
title: 'Add New',
|
||||
to: '/add/new'
|
||||
to: '/add/search'
|
||||
},
|
||||
{
|
||||
title: 'Import',
|
||||
|
||||
@@ -176,12 +176,20 @@ class SignalRConnector extends Component {
|
||||
}
|
||||
|
||||
handleAlbum = (body) => {
|
||||
if (body.action === 'updated') {
|
||||
const action = body.action;
|
||||
const section = 'albums';
|
||||
|
||||
if (action === 'updated') {
|
||||
this.props.dispatchUpdateItem({
|
||||
section: 'albums',
|
||||
section,
|
||||
updateOnly: true,
|
||||
...body.resource
|
||||
});
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({
|
||||
section,
|
||||
id: body.resource.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as filterTypes from './filterTypes';
|
||||
import * as icons from './icons';
|
||||
import * as kinds from './kinds';
|
||||
import * as messageTypes from './messageTypes';
|
||||
import * as metadataProfileNames from './metadataProfileNames';
|
||||
import * as sizes from './sizes';
|
||||
import * as scrollDirections from './scrollDirections';
|
||||
import * as sortDirections from './sortDirections';
|
||||
@@ -22,6 +23,7 @@ export {
|
||||
icons,
|
||||
kinds,
|
||||
messageTypes,
|
||||
metadataProfileNames,
|
||||
sizes,
|
||||
scrollDirections,
|
||||
sortDirections,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const NONE = 'None';
|
||||
File renamed without changes.
+46
-28
@@ -8,10 +8,11 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import AddNewArtistSearchResultConnector from './AddNewArtistSearchResultConnector';
|
||||
import styles from './AddNewArtist.css';
|
||||
import AddNewArtistSearchResultConnector from './Artist/AddNewArtistSearchResultConnector';
|
||||
import AddNewAlbumSearchResultConnector from './Album/AddNewAlbumSearchResultConnector';
|
||||
import styles from './AddNewItem.css';
|
||||
|
||||
class AddNewArtist extends Component {
|
||||
class AddNewItem extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -29,7 +30,7 @@ class AddNewArtist extends Component {
|
||||
const term = this.state.term;
|
||||
|
||||
if (term) {
|
||||
this.props.onArtistLookupChange(term);
|
||||
this.props.onSearchChange(term);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ class AddNewArtist extends Component {
|
||||
term,
|
||||
isFetching: true
|
||||
});
|
||||
this.props.onArtistLookupChange(term);
|
||||
this.props.onSearchChange(term);
|
||||
} else if (isFetching !== prevProps.isFetching) {
|
||||
this.setState({
|
||||
isFetching
|
||||
@@ -60,16 +61,16 @@ class AddNewArtist extends Component {
|
||||
|
||||
this.setState({ term: value, isFetching: hasValue }, () => {
|
||||
if (hasValue) {
|
||||
this.props.onArtistLookupChange(value);
|
||||
this.props.onSearchChange(value);
|
||||
} else {
|
||||
this.props.onClearArtistLookup();
|
||||
this.props.onClearSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClearArtistLookupPress = () => {
|
||||
onClearSearchPress = () => {
|
||||
this.setState({ term: '' });
|
||||
this.props.onClearArtistLookup();
|
||||
this.props.onClearSearch();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -85,7 +86,7 @@ class AddNewArtist extends Component {
|
||||
const isFetching = this.state.isFetching;
|
||||
|
||||
return (
|
||||
<PageContent title="Add New Artist">
|
||||
<PageContent title="Add New Item">
|
||||
<PageContentBodyConnector>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
@@ -97,7 +98,7 @@ class AddNewArtist extends Component {
|
||||
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
name="artistLookup"
|
||||
name="searchBox"
|
||||
value={term}
|
||||
placeholder="eg. Breaking Benjamin, lidarr:854a1807-025b-42a8-ba8c-2a39717f1d25"
|
||||
autoFocus={true}
|
||||
@@ -106,7 +107,7 @@ class AddNewArtist extends Component {
|
||||
|
||||
<Button
|
||||
className={styles.clearLookupButton}
|
||||
onPress={this.onClearArtistLookupPress}
|
||||
onPress={this.onClearSearchPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.REMOVE}
|
||||
@@ -130,12 +131,26 @@ class AddNewArtist extends Component {
|
||||
<div className={styles.searchResults}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<AddNewArtistSearchResultConnector
|
||||
key={item.foreignArtistId}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
if (item.artist) {
|
||||
const artist = item.artist;
|
||||
return (
|
||||
<AddNewArtistSearchResultConnector
|
||||
key={item.id}
|
||||
{...artist}
|
||||
/>
|
||||
);
|
||||
} else if (item.album) {
|
||||
const album = item.album;
|
||||
return (
|
||||
<AddNewAlbumSearchResultConnector
|
||||
key={item.id}
|
||||
isExistingAlbum={'id' in album && album.id !== 0}
|
||||
isExistingArtist={'id' in album.artist && album.artist.id !== 0}
|
||||
{...album}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
@@ -145,11 +160,10 @@ class AddNewArtist extends Component {
|
||||
!isFetching && !error && !items.length && !!term &&
|
||||
<div className={styles.message}>
|
||||
<div className={styles.noResults}>Couldn't find any results for '{term}'</div>
|
||||
<div>You can also search using MusicBrainz ID of an artist. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div>
|
||||
<div>
|
||||
<Link to="https://github.com/Lidarr/Lidarr/wiki/FAQ#why-cant-i-add-a-new-artist-when-i-know-the-musicbrainz-id">
|
||||
Why can't I find my artist?
|
||||
</Link>
|
||||
You can also search using the
|
||||
<Link to="https://musicbrainz.org/search"> MusicBrainz ID </Link>
|
||||
of an artist e.g. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -157,8 +171,12 @@ class AddNewArtist extends Component {
|
||||
{
|
||||
!term &&
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>It's easy to add a new artist, just start typing the name the artist you want to add.</div>
|
||||
<div>You can also search using MusicBrainz ID of an artist. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div>
|
||||
<div className={styles.helpText}>It's easy to add a new artist, just start typing the name of the artist you want to add.</div>
|
||||
<div>
|
||||
You can also search using the
|
||||
<Link to="https://musicbrainz.org/search"> MusicBrainz ID </Link>
|
||||
of an artist e.g. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -169,15 +187,15 @@ class AddNewArtist extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
AddNewArtist.propTypes = {
|
||||
AddNewItem.propTypes = {
|
||||
term: PropTypes.string,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onArtistLookupChange: PropTypes.func.isRequired,
|
||||
onClearArtistLookup: PropTypes.func.isRequired
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
onClearSearch: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewArtist;
|
||||
export default AddNewItem;
|
||||
+27
-27
@@ -3,32 +3,32 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
import { lookupArtist, clearAddArtist } from 'Store/Actions/addArtistActions';
|
||||
import { getSearchResults, clearSearchResults } from 'Store/Actions/searchActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import AddNewArtist from './AddNewArtist';
|
||||
import AddNewItem from './AddNewItem';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addArtist,
|
||||
(state) => state.search,
|
||||
(state) => state.router.location,
|
||||
(addArtist, location) => {
|
||||
(search, location) => {
|
||||
const { params } = parseUrl(location.search);
|
||||
|
||||
return {
|
||||
term: params.term,
|
||||
...addArtist
|
||||
...search
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
lookupArtist,
|
||||
clearAddArtist,
|
||||
getSearchResults,
|
||||
clearSearchResults,
|
||||
fetchRootFolders
|
||||
};
|
||||
|
||||
class AddNewArtistConnector extends Component {
|
||||
class AddNewItemConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -36,7 +36,7 @@ class AddNewArtistConnector extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._artistLookupTimeout = null;
|
||||
this._searchTimeout = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -44,32 +44,32 @@ class AddNewArtistConnector extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._artistLookupTimeout) {
|
||||
clearTimeout(this._artistLookupTimeout);
|
||||
if (this._searchTimeout) {
|
||||
clearTimeout(this._searchTimeout);
|
||||
}
|
||||
|
||||
this.props.clearAddArtist();
|
||||
this.props.clearSearchResults();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onArtistLookupChange = (term) => {
|
||||
if (this._artistLookupTimeout) {
|
||||
clearTimeout(this._artistLookupTimeout);
|
||||
onSearchChange = (term) => {
|
||||
if (this._searchTimeout) {
|
||||
clearTimeout(this._searchTimeout);
|
||||
}
|
||||
|
||||
if (term.trim() === '') {
|
||||
this.props.clearAddArtist();
|
||||
this.props.clearSearchResults();
|
||||
} else {
|
||||
this._artistLookupTimeout = setTimeout(() => {
|
||||
this.props.lookupArtist({ term });
|
||||
this._searchTimeout = setTimeout(() => {
|
||||
this.props.getSearchResults({ term });
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
onClearArtistLookup = () => {
|
||||
this.props.clearAddArtist();
|
||||
onClearSearch = () => {
|
||||
this.props.clearSearchResults();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -82,21 +82,21 @@ class AddNewArtistConnector extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AddNewArtist
|
||||
<AddNewItem
|
||||
term={term}
|
||||
{...otherProps}
|
||||
onArtistLookupChange={this.onArtistLookupChange}
|
||||
onClearArtistLookup={this.onClearArtistLookup}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onClearSearch={this.onClearSearch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewArtistConnector.propTypes = {
|
||||
AddNewItemConnector.propTypes = {
|
||||
term: PropTypes.string,
|
||||
lookupArtist: PropTypes.func.isRequired,
|
||||
clearAddArtist: PropTypes.func.isRequired,
|
||||
getSearchResults: PropTypes.func.isRequired,
|
||||
clearSearchResults: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewArtistConnector);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewItemConnector);
|
||||
@@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddNewAlbumModalContentConnector from './AddNewAlbumModalContentConnector';
|
||||
|
||||
function AddNewAlbumModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddNewAlbumModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddNewAlbumModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewAlbumModal;
|
||||
@@ -0,0 +1,126 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.artistName {
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.disambiguation {
|
||||
margin-bottom: 20px;
|
||||
color: $disabledColor;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-bottom: 30px;
|
||||
max-height: 230px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 300px;
|
||||
}
|
||||
|
||||
.albumType {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.albumTypeLabel {
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.albumCount {
|
||||
color: #8895aa;
|
||||
font-style: italic;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.expandButton {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.searchForNewAlbumLabelContainer {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.searchForNewAlbumLabel {
|
||||
margin-right: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.searchForNewAlbumContainer {
|
||||
composes: container from '~Components/Form/CheckInput.css';
|
||||
|
||||
flex: 0 1 0;
|
||||
}
|
||||
|
||||
.searchForNewAlbumInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
}
|
||||
|
||||
.addButton {
|
||||
@add-mixin truncate;
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalFooter {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
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 AlbumCover from 'Album/AlbumCover';
|
||||
import AddArtistOptionsForm from '../Common/AddArtistOptionsForm.js';
|
||||
import styles from './AddNewAlbumModalContent.css';
|
||||
|
||||
class AddNewAlbumModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
searchForNewAlbum: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchForNewAlbumChange = ({ value }) => {
|
||||
this.setState({ searchForNewAlbum: value });
|
||||
}
|
||||
|
||||
onAddAlbumPress = () => {
|
||||
this.props.onAddAlbumPress(this.state.searchForNewAlbum);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
albumTitle,
|
||||
artistName,
|
||||
disambiguation,
|
||||
overview,
|
||||
images,
|
||||
isAdding,
|
||||
isExistingArtist,
|
||||
isSmallScreen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Add new Album
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
isSmallScreen ?
|
||||
null:
|
||||
<div className={styles.poster}>
|
||||
<AlbumCover
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.name}>
|
||||
{albumTitle}
|
||||
</div>
|
||||
|
||||
{
|
||||
!!disambiguation &&
|
||||
<span className={styles.disambiguation}>({disambiguation})</span>
|
||||
}
|
||||
|
||||
<div>
|
||||
<span className={styles.artistName}> By: {artistName}</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
overview ?
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
truncateText="…"
|
||||
line={8}
|
||||
text={overview}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isExistingArtist &&
|
||||
<AddArtistOptionsForm
|
||||
artistName={artistName}
|
||||
includeNoneMetadataProfile={true}
|
||||
{...otherProps}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<label className={styles.searchForNewAlbumLabelContainer}>
|
||||
<span className={styles.searchForNewAlbumLabel}>
|
||||
Start search for new album
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
containerClassName={styles.searchForNewAlbumContainer}
|
||||
className={styles.searchForNewAlbumInput}
|
||||
name="searchForNewAlbum"
|
||||
value={this.state.searchForNewAlbum}
|
||||
onChange={this.onSearchForNewAlbumChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.addButton}
|
||||
kind={kinds.SUCCESS}
|
||||
isSpinning={isAdding}
|
||||
onPress={this.onAddAlbumPress}
|
||||
>
|
||||
Add {albumTitle}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewAlbumModalContent.propTypes = {
|
||||
albumTitle: PropTypes.string.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
isExistingArtist: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onAddAlbumPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewAlbumModalContent;
|
||||
@@ -0,0 +1,135 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { metadataProfileNames } from 'Helpers/Props';
|
||||
import { setAddDefault, addAlbum } from 'Store/Actions/searchActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import AddNewAlbumModalContent from './AddNewAlbumModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { isExistingArtist }) => isExistingArtist,
|
||||
(state) => state.search,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
createDimensionsSelector(),
|
||||
(isExistingArtist, searchState, metadataProfiles, dimensions) => {
|
||||
const {
|
||||
isAdding,
|
||||
addError,
|
||||
defaults
|
||||
} = searchState;
|
||||
|
||||
const {
|
||||
settings,
|
||||
validationErrors,
|
||||
validationWarnings
|
||||
} = selectSettings(defaults, {}, addError);
|
||||
|
||||
// For adding single albums, default to None profile
|
||||
const noneProfile = metadataProfiles.items.find((item) => item.name === metadataProfileNames.NONE);
|
||||
|
||||
return {
|
||||
isAdding,
|
||||
addError,
|
||||
showMetadataProfile: true,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
noneMetadataProfileId: noneProfile.id,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setAddDefault,
|
||||
addAlbum
|
||||
};
|
||||
|
||||
class AddNewAlbumModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
metadataProfileIdDefault: props.metadataProfileId.value
|
||||
};
|
||||
|
||||
// select none as default
|
||||
this.onInputChange({
|
||||
name: 'metadataProfileId',
|
||||
value: props.noneMetadataProfileId
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// reinstate standard default
|
||||
this.props.setAddDefault({ metadataProfileId: this.state.metadataProfileIdDefault });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setAddDefault({ [name]: value });
|
||||
}
|
||||
|
||||
onAddAlbumPress = (searchForNewAlbum) => {
|
||||
const {
|
||||
foreignAlbumId,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
albumFolder,
|
||||
tags
|
||||
} = this.props;
|
||||
|
||||
this.props.addAlbum({
|
||||
foreignAlbumId,
|
||||
rootFolderPath: rootFolderPath.value,
|
||||
monitor: monitor.value,
|
||||
qualityProfileId: qualityProfileId.value,
|
||||
metadataProfileId: metadataProfileId.value,
|
||||
albumFolder: albumFolder.value,
|
||||
tags: tags.value,
|
||||
searchForNewAlbum
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddNewAlbumModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onAddAlbumPress={this.onAddAlbumPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewAlbumModalContentConnector.propTypes = {
|
||||
isExistingArtist: PropTypes.bool.isRequired,
|
||||
foreignAlbumId: PropTypes.string.isRequired,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
metadataProfileId: PropTypes.object,
|
||||
noneMetadataProfileId: PropTypes.number.isRequired,
|
||||
albumFolder: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
setAddDefault: PropTypes.func.isRequired,
|
||||
addAlbum: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewAlbumModalContentConnector);
|
||||
@@ -0,0 +1,64 @@
|
||||
.searchResult {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
color: inherit;
|
||||
transition: background 500ms;
|
||||
|
||||
&:hover {
|
||||
background-color: #eaf2ff;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 0 1 100%;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.artistName {
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.year {
|
||||
margin-left: 10px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.mbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin-top: -4px;
|
||||
margin-left: auto;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.mbLinkIcon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.alreadyExistsIcon {
|
||||
margin-left: 10px;
|
||||
color: #37bc9b;
|
||||
}
|
||||
|
||||
.overview {
|
||||
overflow: hidden;
|
||||
margin-top: 20px;
|
||||
text-align: justify;
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import { icons, sizes } from 'Helpers/Props';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import AlbumCover from 'Album/AlbumCover';
|
||||
import AddNewAlbumModal from './AddNewAlbumModal';
|
||||
import styles from './AddNewAlbumSearchResult.css';
|
||||
|
||||
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen);
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
function calculateHeight(rowHeight, isSmallScreen) {
|
||||
let height = rowHeight - 70;
|
||||
|
||||
if (isSmallScreen) {
|
||||
height -= columnPaddingSmallScreen;
|
||||
} else {
|
||||
height -= columnPadding;
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
class AddNewAlbumSearchResult extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isNewAddAlbumModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.isExistingAlbum && this.props.isExistingAlbum) {
|
||||
this.onAddAlbumModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.setState({ isNewAddAlbumModalOpen: true });
|
||||
}
|
||||
|
||||
onAddAlbumModalClose = () => {
|
||||
this.setState({ isNewAddAlbumModalOpen: false });
|
||||
}
|
||||
|
||||
onMBLinkPress = (event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
foreignAlbumId,
|
||||
title,
|
||||
releaseDate,
|
||||
disambiguation,
|
||||
albumType,
|
||||
secondaryTypes,
|
||||
overview,
|
||||
ratings,
|
||||
images,
|
||||
releases,
|
||||
artist,
|
||||
isExistingAlbum,
|
||||
isExistingArtist,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isNewAddAlbumModalOpen
|
||||
} = this.state;
|
||||
|
||||
const linkProps = isExistingAlbum ? { to: `/album/${foreignAlbumId}` } : { onPress: this.onPress };
|
||||
|
||||
const height = calculateHeight(230, isSmallScreen);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
className={styles.searchResult}
|
||||
{...linkProps}
|
||||
>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<AlbumCover
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
}
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name}>
|
||||
{title}
|
||||
|
||||
{
|
||||
!!disambiguation &&
|
||||
<span className={styles.year}>({disambiguation})</span>
|
||||
}
|
||||
|
||||
{
|
||||
isExistingAlbum ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={20}
|
||||
title="Album already in your library"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.mbLink}
|
||||
to={`https://musicbrainz.org/release-group/${foreignAlbumId}`}
|
||||
onPress={this.onMBLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.mbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className={styles.artistName}> By: {artist.artistName}</span>
|
||||
|
||||
{
|
||||
isExistingArtist ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={15}
|
||||
title="Artist already in your library"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label size={sizes.LARGE}>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
iconSize={13}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{
|
||||
!!releaseDate &&
|
||||
<Label size={sizes.LARGE}>
|
||||
{moment(releaseDate).format('YYYY')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
<Label size={sizes.LARGE}>
|
||||
{releases.length} release{releases.length > 0 ? 's' : null}
|
||||
</Label>
|
||||
|
||||
{
|
||||
!!albumType &&
|
||||
<Label size={sizes.LARGE}>
|
||||
{albumType}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
!!secondaryTypes &&
|
||||
secondaryTypes.map((item, i) => {
|
||||
return (
|
||||
<Label
|
||||
size={sizes.LARGE}
|
||||
key={i}
|
||||
>
|
||||
{item}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.overview}
|
||||
style={{
|
||||
maxHeight: `${height}px`
|
||||
}}
|
||||
>
|
||||
<TextTruncate
|
||||
truncateText="…"
|
||||
line={Math.floor(height / (defaultFontSize * lineHeight))}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<AddNewAlbumModal
|
||||
isOpen={isNewAddAlbumModalOpen && !isExistingAlbum}
|
||||
isExistingArtist={isExistingArtist}
|
||||
foreignAlbumId={foreignAlbumId}
|
||||
albumTitle={title}
|
||||
disambiguation={disambiguation}
|
||||
artistName={artist.artistName}
|
||||
overview={overview}
|
||||
images={images}
|
||||
onModalClose={this.onAddAlbumModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewAlbumSearchResult.propTypes = {
|
||||
foreignAlbumId: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string,
|
||||
albumType: PropTypes.string,
|
||||
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
overview: PropTypes.string,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
artist: PropTypes.object,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
releases: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExistingAlbum: PropTypes.bool.isRequired,
|
||||
isExistingArtist: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AddNewAlbumSearchResult;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import AddNewAlbumSearchResult from './AddNewAlbumSearchResult';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createDimensionsSelector(),
|
||||
(dimensions) => {
|
||||
return {
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(AddNewAlbumSearchResult);
|
||||
File renamed without changes.
+12
-10
@@ -17,16 +17,24 @@
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.disambiguation {
|
||||
margin-bottom: 20px;
|
||||
color: $disabledColor;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-bottom: 30px;
|
||||
max-height: 230px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.searchForMissingAlbumsLabelContainer {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
@@ -58,12 +66,6 @@
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
}
|
||||
|
||||
.hideMetadataProfile {
|
||||
composes: group from '~Components/Form/FormGroup.css';
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalFooter {
|
||||
display: block;
|
||||
@@ -0,0 +1,146 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
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 ArtistPoster from 'Artist/ArtistPoster';
|
||||
import AddArtistOptionsForm from '../Common/AddArtistOptionsForm.js';
|
||||
import styles from './AddNewArtistModalContent.css';
|
||||
|
||||
class AddNewArtistModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
searchForMissingAlbums: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchForMissingAlbumsChange = ({ value }) => {
|
||||
this.setState({ searchForMissingAlbums: value });
|
||||
}
|
||||
|
||||
onAddArtistPress = () => {
|
||||
this.props.onAddArtistPress(this.state.searchForMissingAlbums);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
artistName,
|
||||
disambiguation,
|
||||
overview,
|
||||
images,
|
||||
isAdding,
|
||||
isSmallScreen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Add new Artist
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
isSmallScreen ?
|
||||
null:
|
||||
<div className={styles.poster}>
|
||||
<ArtistPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.name}>
|
||||
{artistName}
|
||||
</div>
|
||||
|
||||
{
|
||||
!!disambiguation &&
|
||||
<span className={styles.disambiguation}>({disambiguation})</span>
|
||||
}
|
||||
|
||||
{
|
||||
overview ?
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
truncateText="…"
|
||||
line={8}
|
||||
text={overview}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<AddArtistOptionsForm
|
||||
includeNoneMetadataProfile={false}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<label className={styles.searchForMissingAlbumsLabelContainer}>
|
||||
<span className={styles.searchForMissingAlbumsLabel}>
|
||||
Start search for missing albums
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
containerClassName={styles.searchForMissingAlbumsContainer}
|
||||
className={styles.searchForMissingAlbumsInput}
|
||||
name="searchForMissingAlbums"
|
||||
value={this.state.searchForMissingAlbums}
|
||||
onChange={this.onSearchForMissingAlbumsChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.addButton}
|
||||
kind={kinds.SUCCESS}
|
||||
isSpinning={isAdding}
|
||||
onPress={this.onAddArtistPress}
|
||||
>
|
||||
Add {artistName}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewArtistModalContent.propTypes = {
|
||||
artistName: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onAddArtistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewArtistModalContent;
|
||||
+8
-8
@@ -2,22 +2,22 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setAddArtistDefault, addArtist } from 'Store/Actions/addArtistActions';
|
||||
import { setAddDefault, addArtist } from 'Store/Actions/searchActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import AddNewArtistModalContent from './AddNewArtistModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addArtist,
|
||||
(state) => state.search,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
createDimensionsSelector(),
|
||||
(addArtistState, metadataProfiles, dimensions) => {
|
||||
(searchState, metadataProfiles, dimensions) => {
|
||||
const {
|
||||
isAdding,
|
||||
addError,
|
||||
defaults
|
||||
} = addArtistState;
|
||||
} = searchState;
|
||||
|
||||
const {
|
||||
settings,
|
||||
@@ -28,7 +28,7 @@ function createMapStateToProps() {
|
||||
return {
|
||||
isAdding,
|
||||
addError,
|
||||
showMetadataProfile: metadataProfiles.items.length > 1,
|
||||
showMetadataProfile: metadataProfiles.items.length > 2, // NONE (not allowed for artists) and one other
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
@@ -39,7 +39,7 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setAddArtistDefault,
|
||||
setAddDefault,
|
||||
addArtist
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ class AddNewArtistModalContentConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setAddArtistDefault({ [name]: value });
|
||||
this.props.setAddDefault({ [name]: value });
|
||||
}
|
||||
|
||||
onAddArtistPress = (searchForMissingAlbums) => {
|
||||
@@ -98,7 +98,7 @@ AddNewArtistModalContentConnector.propTypes = {
|
||||
albumFolder: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
setAddArtistDefault: PropTypes.func.isRequired,
|
||||
setAddDefault: PropTypes.func.isRequired,
|
||||
addArtist: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
File renamed without changes.
+1
@@ -196,6 +196,7 @@ class AddNewArtistSearchResult extends Component {
|
||||
isOpen={isNewAddArtistModalOpen && !isExistingArtist}
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
disambiguation={disambiguation}
|
||||
year={year}
|
||||
overview={overview}
|
||||
images={images}
|
||||
File renamed without changes.
@@ -0,0 +1,9 @@
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.hideMetadataProfile {
|
||||
composes: group from '~Components/Form/FormGroup.css';
|
||||
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, inputTypes, tooltipPositions } from 'Helpers/Props';
|
||||
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 Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent';
|
||||
import ArtistMetadataProfilePopoverContent from 'AddArtist/ArtistMetadataProfilePopoverContent';
|
||||
import styles from './AddArtistOptionsForm.css';
|
||||
|
||||
class AddArtistOptionsForm extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQualityProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||
}
|
||||
|
||||
onMetadataProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'metadataProfileId', value: parseInt(value) });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
includeNoneMetadataProfile,
|
||||
showMetadataProfile,
|
||||
albumFolder,
|
||||
tags,
|
||||
onInputChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Root Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
onChange={onInputChange}
|
||||
{...rootFolderPath}
|
||||
/>
|
||||
</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="monitor"
|
||||
onChange={onInputChange}
|
||||
{...monitor}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Quality Profile</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
onChange={this.onQualityProfileIdChange}
|
||||
{...qualityProfileId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
|
||||
<FormLabel>
|
||||
Metadata Profile
|
||||
|
||||
{
|
||||
includeNoneMetadataProfile &&
|
||||
<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="metadataProfileId"
|
||||
includeNone={includeNoneMetadataProfile}
|
||||
onChange={this.onMetadataProfileIdChange}
|
||||
{...metadataProfileId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Album Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="albumFolder"
|
||||
onChange={onInputChange}
|
||||
{...albumFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
onChange={onInputChange}
|
||||
{...tags}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddArtistOptionsForm.propTypes = {
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
metadataProfileId: PropTypes.object,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
includeNoneMetadataProfile: PropTypes.bool.isRequired,
|
||||
albumFolder: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddArtistOptionsForm;
|
||||
+3
-3
@@ -58,12 +58,12 @@ function EditImportListExclusionModalContent(props) {
|
||||
{...otherProps}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>Artist Name</FormLabel>
|
||||
<FormLabel>Entity Name</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="artistName"
|
||||
helpText="The name of the artist to exclude (can be anything meaningful)"
|
||||
helpText="The name of the artist/album to exclude (can be anything meaningful)"
|
||||
{...artistName}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
@@ -75,7 +75,7 @@ function EditImportListExclusionModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="foreignId"
|
||||
helpText="The Musicbrainz Id of the artist to exclude"
|
||||
helpText="The Musicbrainz Id of the artist/album to exclude"
|
||||
{...foreignId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { icons, metadataProfileNames } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Card from 'Components/Card';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -58,7 +58,7 @@ class MetadataProfiles extends Component {
|
||||
>
|
||||
<div className={styles.metadataProfiles}>
|
||||
{
|
||||
items.sort(sortByName).map((item) => {
|
||||
items.filter((item) => item.name !== metadataProfileNames.NONE).sort(sortByName).map((item) => {
|
||||
return (
|
||||
<MetadataProfile
|
||||
key={item.id}
|
||||
|
||||
@@ -10,6 +10,7 @@ import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptio
|
||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||
import albumEntities from 'Album/albumEntities';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { updateItem } from './baseActions';
|
||||
|
||||
@@ -113,6 +114,7 @@ export const SET_ALBUMS_TABLE_OPTION = 'albums/setAlbumsTableOption';
|
||||
export const CLEAR_ALBUMS = 'albums/clearAlbums';
|
||||
export const SET_ALBUM_VALUE = 'albums/setAlbumValue';
|
||||
export const SAVE_ALBUM = 'albums/saveAlbum';
|
||||
export const DELETE_ALBUM = 'albums/deleteAlbum';
|
||||
export const TOGGLE_ALBUM_MONITORED = 'albums/toggleAlbumMonitored';
|
||||
export const TOGGLE_ALBUMS_MONITORED = 'albums/toggleAlbumsMonitored';
|
||||
|
||||
@@ -128,6 +130,16 @@ export const toggleAlbumsMonitored = createThunk(TOGGLE_ALBUMS_MONITORED);
|
||||
|
||||
export const saveAlbum = createThunk(SAVE_ALBUM);
|
||||
|
||||
export const deleteAlbum = createThunk(DELETE_ALBUM, (payload) => {
|
||||
return {
|
||||
...payload,
|
||||
queryParams: {
|
||||
deleteFiles: payload.deleteFiles,
|
||||
addImportListExclusion: payload.addImportListExclusion
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const setAlbumValue = createAction(SET_ALBUM_VALUE, (payload) => {
|
||||
return {
|
||||
section: 'albums',
|
||||
@@ -141,6 +153,7 @@ export const setAlbumValue = createAction(SET_ALBUM_VALUE, (payload) => {
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_ALBUMS]: createFetchHandler(section, '/album'),
|
||||
[SAVE_ALBUM]: createSaveProviderHandler(section, '/album'),
|
||||
[DELETE_ALBUM]: createRemoveItemHandler(section, '/album'),
|
||||
|
||||
[TOGGLE_ALBUM_MONITORED]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as addArtist from './addArtistActions';
|
||||
import * as app from './appActions';
|
||||
import * as blacklist from './blacklistActions';
|
||||
import * as calendar from './calendarActions';
|
||||
@@ -24,6 +23,7 @@ import * as artist from './artistActions';
|
||||
import * as artistEditor from './artistEditorActions';
|
||||
import * as artistHistory from './artistHistoryActions';
|
||||
import * as artistIndex from './artistIndexActions';
|
||||
import * as search from './searchActions';
|
||||
import * as settings from './settingsActions';
|
||||
import * as system from './systemActions';
|
||||
import * as tags from './tagActions';
|
||||
@@ -31,7 +31,6 @@ import * as tracks from './trackActions';
|
||||
import * as wanted from './wantedActions';
|
||||
|
||||
export default [
|
||||
addArtist,
|
||||
app,
|
||||
blacklist,
|
||||
captcha,
|
||||
@@ -57,6 +56,7 @@ export default [
|
||||
artistEditor,
|
||||
artistHistory,
|
||||
artistIndex,
|
||||
search,
|
||||
settings,
|
||||
system,
|
||||
tags,
|
||||
|
||||
+60
-26
@@ -6,15 +6,15 @@ import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getNewArtist from 'Utilities/Artist/getNewArtist';
|
||||
import getNewAlbum from 'Utilities/Album/getNewAlbum';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { set, update, updateItem } from './baseActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'addArtist';
|
||||
export const section = 'search';
|
||||
let abortCurrentRequest = null;
|
||||
|
||||
//
|
||||
@@ -40,39 +40,33 @@ export const defaultState = {
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'addArtist.defaults'
|
||||
'search.defaults'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const LOOKUP_ARTIST = 'addArtist/lookupArtist';
|
||||
export const ADD_ARTIST = 'addArtist/addArtist';
|
||||
export const SET_ADD_ARTIST_VALUE = 'addArtist/setAddArtistValue';
|
||||
export const CLEAR_ADD_ARTIST = 'addArtist/clearAddArtist';
|
||||
export const SET_ADD_ARTIST_DEFAULT = 'addArtist/setAddArtistDefault';
|
||||
export const GET_SEARCH_RESULTS = 'search/getSearchResults';
|
||||
export const ADD_ARTIST = 'search/addArtist';
|
||||
export const ADD_ALBUM = 'search/addAlbum';
|
||||
export const CLEAR_SEARCH_RESULTS = 'search/clearSearchResults';
|
||||
export const SET_ADD_DEFAULT = 'search/setAddDefault';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const lookupArtist = createThunk(LOOKUP_ARTIST);
|
||||
export const getSearchResults = createThunk(GET_SEARCH_RESULTS);
|
||||
export const addArtist = createThunk(ADD_ARTIST);
|
||||
export const clearAddArtist = createAction(CLEAR_ADD_ARTIST);
|
||||
export const setAddArtistDefault = createAction(SET_ADD_ARTIST_DEFAULT);
|
||||
|
||||
export const setAddArtistValue = createAction(SET_ADD_ARTIST_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
export const addAlbum = createThunk(ADD_ALBUM);
|
||||
export const clearSearchResults = createAction(CLEAR_SEARCH_RESULTS);
|
||||
export const setAddDefault = createAction(SET_ADD_DEFAULT);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[LOOKUP_ARTIST]: function(getState, payload, dispatch) {
|
||||
[GET_SEARCH_RESULTS]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isFetching: true }));
|
||||
|
||||
if (abortCurrentRequest) {
|
||||
@@ -80,7 +74,7 @@ export const actionHandlers = handleThunks({
|
||||
}
|
||||
|
||||
const { request, abortRequest } = createAjaxRequest({
|
||||
url: '/artist/lookup',
|
||||
url: '/search',
|
||||
data: {
|
||||
term: payload.term
|
||||
}
|
||||
@@ -115,8 +109,9 @@ export const actionHandlers = handleThunks({
|
||||
dispatch(set({ section, isAdding: true }));
|
||||
|
||||
const foreignArtistId = payload.foreignArtistId;
|
||||
const items = getState().addArtist.items;
|
||||
const newArtist = getNewArtist(_.cloneDeep(_.find(items, { foreignArtistId })), payload);
|
||||
const items = getState().search.items;
|
||||
const itemToAdd = _.find(items, { foreignId: foreignArtistId });
|
||||
const newArtist = getNewArtist(_.cloneDeep(itemToAdd.artist), payload);
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/artist',
|
||||
@@ -138,6 +133,47 @@ export const actionHandlers = handleThunks({
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isAdding: false,
|
||||
isAdded: false,
|
||||
addError: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[ADD_ALBUM]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isAdding: true }));
|
||||
|
||||
const foreignAlbumId = payload.foreignAlbumId;
|
||||
const items = getState().search.items;
|
||||
const itemToAdd = _.find(items, { foreignId: foreignAlbumId });
|
||||
const newAlbum = getNewAlbum(_.cloneDeep(itemToAdd.album), payload);
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/album',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(newAlbum)
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
data.releases = itemToAdd.album.releases;
|
||||
itemToAdd.album = data;
|
||||
dispatch(batchActions([
|
||||
updateItem({ section: 'artist', ...data.artist }),
|
||||
updateItem({ section, ...itemToAdd }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isAdding: false,
|
||||
isAdded: true,
|
||||
addError: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
@@ -154,9 +190,7 @@ export const actionHandlers = handleThunks({
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_ADD_ARTIST_VALUE]: createSetSettingValueReducer(section),
|
||||
|
||||
[SET_ADD_ARTIST_DEFAULT]: function(state, { payload }) {
|
||||
[SET_ADD_DEFAULT]: function(state, { payload }) {
|
||||
const newState = getSectionState(state, section);
|
||||
|
||||
newState.defaults = {
|
||||
@@ -167,7 +201,7 @@ export const reducers = createHandleActions({
|
||||
return updateSectionState(state, section, newState);
|
||||
},
|
||||
|
||||
[CLEAR_ADD_ARTIST]: function(state) {
|
||||
[CLEAR_SEARCH_RESULTS]: function(state) {
|
||||
const {
|
||||
defaults,
|
||||
...otherDefaultState
|
||||
@@ -0,0 +1,18 @@
|
||||
import getNewArtist from 'Utilities/Artist/getNewArtist';
|
||||
|
||||
function getNewAlbum(album, payload) {
|
||||
const {
|
||||
searchForNewAlbum = false
|
||||
} = payload;
|
||||
|
||||
getNewArtist(album.artist, payload);
|
||||
|
||||
album.addOptions = {
|
||||
searchForNewAlbum
|
||||
};
|
||||
album.monitored = true;
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
export default getNewAlbum;
|
||||
@@ -14,31 +14,51 @@
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace Lidarr.Api.V1.Albums
|
||||
{
|
||||
public class AlbumModule : AlbumModuleWithSignalR,
|
||||
IHandle<AlbumGrabbedEvent>,
|
||||
IHandle<AlbumEditedEvent>,
|
||||
IHandle<AlbumUpdatedEvent>,
|
||||
IHandle<AlbumImportedEvent>,
|
||||
IHandle<TrackImportedEvent>,
|
||||
IHandle<TrackFileDeletedEvent>
|
||||
|
||||
{
|
||||
protected readonly IReleaseService _releaseService;
|
||||
protected readonly IAddAlbumService _addAlbumService;
|
||||
|
||||
public AlbumModule(IAlbumService albumService,
|
||||
IAddAlbumService addAlbumService,
|
||||
IReleaseService releaseService,
|
||||
IArtistStatisticsService artistStatisticsService,
|
||||
IMapCoversToLocal coverMapper,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
IBroadcastSignalRMessage signalRBroadcaster)
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
ProfileExistsValidator profileExistsValidator,
|
||||
MetadataProfileExistsValidator metadataProfileExistsValidator)
|
||||
|
||||
: base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
|
||||
{
|
||||
_releaseService = releaseService;
|
||||
_addAlbumService = addAlbumService;
|
||||
|
||||
GetResourceAll = GetAlbums;
|
||||
CreateResource = AddAlbum;
|
||||
UpdateResource = UpdateAlbum;
|
||||
DeleteResource = DeleteAlbum;
|
||||
Put("/monitor", x => SetAlbumsMonitored());
|
||||
|
||||
PostValidator.RuleFor(s => s.ForeignAlbumId).NotEmpty();
|
||||
PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(profileExistsValidator);
|
||||
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();
|
||||
}
|
||||
|
||||
private List<AlbumResource> GetAlbums()
|
||||
@@ -66,6 +86,11 @@ private List<AlbumResource> GetAlbums()
|
||||
|
||||
var album = _albumService.FindById(foreignAlbumId);
|
||||
|
||||
if (album == null)
|
||||
{
|
||||
return MapToResource(new List<Album>(), false);
|
||||
}
|
||||
|
||||
if (includeAllArtistAlbumsQuery.HasValue && Convert.ToBoolean(includeAllArtistAlbumsQuery.Value))
|
||||
{
|
||||
return MapToResource(_albumService.GetAlbumsByArtist(album.ArtistId), false);
|
||||
@@ -85,6 +110,13 @@ private List<AlbumResource> GetAlbums()
|
||||
return MapToResource(_albumService.GetAlbums(albumIds), false);
|
||||
}
|
||||
|
||||
private int AddAlbum(AlbumResource albumResource)
|
||||
{
|
||||
var album = _addAlbumService.AddAlbum(albumResource.ToModel());
|
||||
|
||||
return album.Id;
|
||||
}
|
||||
|
||||
private void UpdateAlbum(AlbumResource albumResource)
|
||||
{
|
||||
var album = _albumService.GetAlbum(albumResource.Id);
|
||||
@@ -97,6 +129,14 @@ private void UpdateAlbum(AlbumResource albumResource)
|
||||
BroadcastResourceChange(ModelAction.Updated, model.Id);
|
||||
}
|
||||
|
||||
private void DeleteAlbum(int id)
|
||||
{
|
||||
var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles");
|
||||
var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion");
|
||||
|
||||
_albumService.DeleteAlbum(id, deleteFiles, addImportListExclusion);
|
||||
}
|
||||
|
||||
private object SetAlbumsMonitored()
|
||||
{
|
||||
var resource = Request.Body.FromJson<AlbumsMonitoredResource>();
|
||||
@@ -122,6 +162,16 @@ public void Handle(AlbumEditedEvent message)
|
||||
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
|
||||
}
|
||||
|
||||
public void Handle(AlbumUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
|
||||
}
|
||||
|
||||
public void Handle(AlbumDeletedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Deleted, message.Album.ToResource());
|
||||
}
|
||||
|
||||
public void Handle(AlbumImportedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
|
||||
|
||||
@@ -43,7 +43,7 @@ public int MediumCount
|
||||
public List<MediaCover> Images { get; set; }
|
||||
public List<Links> Links { get; set; }
|
||||
public AlbumStatisticsResource Statistics { get; set; }
|
||||
|
||||
public AddAlbumOptions AddOptions { get; set; }
|
||||
public string RemoteCover { get; set; }
|
||||
|
||||
//Hiding this so people don't think its usable (only used to set the initial state)
|
||||
@@ -88,6 +88,8 @@ public static Album ToModel(this AlbumResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
var artist = resource.Artist?.ToModel() ?? new NzbDrone.Core.Music.Artist();
|
||||
|
||||
return new Album
|
||||
{
|
||||
Id = resource.Id,
|
||||
@@ -96,9 +98,13 @@ public static Album ToModel(this AlbumResource resource)
|
||||
Disambiguation = resource.Disambiguation,
|
||||
Overview = resource.Overview,
|
||||
Images = resource.Images,
|
||||
AlbumType = resource.AlbumType,
|
||||
Monitored = resource.Monitored,
|
||||
AnyReleaseOk = resource.AnyReleaseOk,
|
||||
AlbumReleases = resource.Releases.ToModel()
|
||||
AlbumReleases = resource.Releases.ToModel(),
|
||||
AddOptions = resource.AddOptions,
|
||||
Artist = artist,
|
||||
ArtistMetadata = artist.Metadata.Value
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ public class MetadataProfileModule : LidarrRestModule<MetadataProfileResource>
|
||||
public MetadataProfileModule(IMetadataProfileService profileService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Name).NotEqual("None").WithMessage("'None' is a reserved profile name").NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.PrimaryAlbumTypes).MustHaveAllowedPrimaryType();
|
||||
SharedValidator.RuleFor(c => c.SecondaryAlbumTypes).MustHaveAllowedSecondaryType();
|
||||
SharedValidator.RuleFor(c => c.ReleaseStatuses).MustHaveAllowedReleaseStatus();
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using Lidarr.Http;
|
||||
using Lidarr.Api.V1.Artist;
|
||||
using Lidarr.Api.V1.Albums;
|
||||
using System;
|
||||
|
||||
namespace Lidarr.Api.V1.Search
|
||||
{
|
||||
public class SearchModule : LidarrRestModule<SearchResource>
|
||||
{
|
||||
private readonly ISearchForNewEntity _searchProxy;
|
||||
|
||||
public SearchModule(ISearchForNewEntity searchProxy)
|
||||
: base("/search")
|
||||
{
|
||||
_searchProxy = searchProxy;
|
||||
Get("/", x => Search());
|
||||
}
|
||||
|
||||
private object Search()
|
||||
{
|
||||
var searchResults = _searchProxy.SearchForNewEntity((string)Request.Query.term);
|
||||
return MapToResource(searchResults).ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<SearchResource> MapToResource(IEnumerable<Object> results)
|
||||
{
|
||||
int id = 1;
|
||||
foreach (var result in results)
|
||||
{
|
||||
var resource = new SearchResource();
|
||||
resource.Id = id++;
|
||||
|
||||
if (result is NzbDrone.Core.Music.Artist)
|
||||
{
|
||||
var artist = (NzbDrone.Core.Music.Artist) result;
|
||||
resource.Artist = artist.ToResource();
|
||||
resource.ForeignId = artist.ForeignArtistId;
|
||||
|
||||
var poster = artist.Metadata.Value.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster);
|
||||
if (poster != null)
|
||||
{
|
||||
resource.Artist.RemotePoster = poster.Url;
|
||||
}
|
||||
}
|
||||
else if (result is NzbDrone.Core.Music.Album)
|
||||
{
|
||||
var album = (NzbDrone.Core.Music.Album) result;
|
||||
resource.Album = album.ToResource();
|
||||
resource.ForeignId = album.ForeignAlbumId;
|
||||
|
||||
var cover = album.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Cover);
|
||||
if (cover != null)
|
||||
{
|
||||
resource.Album.RemoteCover = cover.Url;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException("Bad response from search all proxy");
|
||||
}
|
||||
|
||||
yield return resource;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Lidarr.Http.REST;
|
||||
using Lidarr.Api.V1.Artist;
|
||||
using Lidarr.Api.V1.Albums;
|
||||
|
||||
namespace Lidarr.Api.V1.Search
|
||||
{
|
||||
public class
|
||||
SearchResource : RestResource
|
||||
{
|
||||
public string ForeignId { get; set; }
|
||||
public ArtistResource Artist { get; set; }
|
||||
public AlbumResource Album { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public void add_artist_page()
|
||||
|
||||
page.WaitForNoSpinner();
|
||||
|
||||
page.Find(By.CssSelector("input[class*='AddNewArtist-searchInput']")).Should().NotBeNull();
|
||||
page.Find(By.CssSelector("input[class*='AddNewItem-searchInput']")).Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ public void should_unmap_tracked_download_if_album_deleted()
|
||||
.Returns(default(RemoteAlbum));
|
||||
|
||||
// handle deletion event
|
||||
Subject.Handle(new AlbumDeletedEvent(remoteAlbum.Albums.First(), false));
|
||||
Subject.Handle(new AlbumDeletedEvent(remoteAlbum.Albums.First(), false, false));
|
||||
|
||||
// verify download has null remote album
|
||||
var trackedDownloads = Subject.GetTrackedDownloads();
|
||||
|
||||
@@ -72,6 +72,13 @@ private void WithExistingArtist()
|
||||
.Returns(new Artist{ForeignArtistId = _importListReports.First().ArtistMusicBrainzId });
|
||||
}
|
||||
|
||||
private void WithExistingAlbum()
|
||||
{
|
||||
Mocker.GetMock<IAlbumService>()
|
||||
.Setup(v => v.FindById(_importListReports.First().AlbumMusicBrainzId))
|
||||
.Returns(new Album{ForeignAlbumId = _importListReports.First().AlbumMusicBrainzId });
|
||||
}
|
||||
|
||||
private void WithExcludedArtist()
|
||||
{
|
||||
Mocker.GetMock<IImportListExclusionService>()
|
||||
@@ -83,6 +90,17 @@ private void WithExcludedArtist()
|
||||
});
|
||||
}
|
||||
|
||||
private void WithExcludedAlbum()
|
||||
{
|
||||
Mocker.GetMock<IImportListExclusionService>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<ImportListExclusion> {
|
||||
new ImportListExclusion {
|
||||
ForeignId = "09474d62-17dd-3a4f-98fb-04c65f38a479"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void WithMonitorType(ImportListMonitorType monitor)
|
||||
{
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
@@ -120,17 +138,7 @@ public void should_search_if_album_title_and_no_album_id()
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_with_lidarr_id_if_album_id_and_no_artist_id()
|
||||
{
|
||||
WithAlbumId();
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
|
||||
Mocker.GetMock<ISearchForNewAlbum>()
|
||||
.Verify(v => v.SearchForNewAlbum($"lidarr:{_importListReports.First().AlbumMusicBrainzId}", null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_search_if_album_id_and_artist_id()
|
||||
public void should_not_search_if_album_title_and_album_id()
|
||||
{
|
||||
WithArtistId();
|
||||
WithAlbumId();
|
||||
@@ -159,8 +167,6 @@ public void should_not_search_if_all_info()
|
||||
public void should_not_add_if_existing_artist()
|
||||
{
|
||||
WithArtistId();
|
||||
WithAlbum();
|
||||
WithAlbumId();
|
||||
WithExistingArtist();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
@@ -169,14 +175,37 @@ public void should_not_add_if_existing_artist()
|
||||
.Verify(v => v.AddArtists(It.Is<List<Artist>>(t=>t.Count == 0)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_add_if_existing_album()
|
||||
{
|
||||
WithAlbumId();
|
||||
WithExistingAlbum();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
|
||||
Mocker.GetMock<IAddArtistService>()
|
||||
.Verify(v => v.AddArtists(It.Is<List<Artist>>(t=>t.Count == 0)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_if_existing_artist_but_new_album()
|
||||
{
|
||||
WithAlbumId();
|
||||
WithExistingArtist();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
|
||||
Mocker.GetMock<IAddAlbumService>()
|
||||
.Verify(v => v.AddAlbums(It.Is<List<Album>>(t=>t.Count == 1)));
|
||||
}
|
||||
|
||||
|
||||
[TestCase(ImportListMonitorType.None, false)]
|
||||
[TestCase(ImportListMonitorType.SpecificAlbum, true)]
|
||||
[TestCase(ImportListMonitorType.EntireArtist, true)]
|
||||
public void should_add_if_not_existing_artist(ImportListMonitorType monitor, bool expectedArtistMonitored)
|
||||
{
|
||||
WithArtistId();
|
||||
WithAlbum();
|
||||
WithAlbumId();
|
||||
WithMonitorType(monitor);
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
@@ -185,12 +214,24 @@ public void should_add_if_not_existing_artist(ImportListMonitorType monitor, boo
|
||||
.Verify(v => v.AddArtists(It.Is<List<Artist>>(t => t.Count == 1 && t.First().Monitored == expectedArtistMonitored)));
|
||||
}
|
||||
|
||||
[TestCase(ImportListMonitorType.None, false)]
|
||||
[TestCase(ImportListMonitorType.SpecificAlbum, true)]
|
||||
[TestCase(ImportListMonitorType.EntireArtist, true)]
|
||||
public void should_add_if_not_existing_album(ImportListMonitorType monitor, bool expectedAlbumMonitored)
|
||||
{
|
||||
WithAlbumId();
|
||||
WithMonitorType(monitor);
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
|
||||
Mocker.GetMock<IAddAlbumService>()
|
||||
.Verify(v => v.AddAlbums(It.Is<List<Album>>(t => t.Count == 1 && t.First().Monitored == expectedAlbumMonitored)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_add_if_excluded_artist()
|
||||
public void should_not_add_artist_if_excluded_artist()
|
||||
{
|
||||
WithArtistId();
|
||||
WithAlbum();
|
||||
WithAlbumId();
|
||||
WithExcludedArtist();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
@@ -200,42 +241,28 @@ public void should_not_add_if_excluded_artist()
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_mark_album_for_monitor_if_album_id_and_specific_monitor_selected()
|
||||
public void should_not_add_album_if_excluded_album()
|
||||
{
|
||||
WithArtistId();
|
||||
WithAlbum();
|
||||
WithAlbumId();
|
||||
WithMonitorType(ImportListMonitorType.SpecificAlbum);
|
||||
WithExcludedAlbum();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
|
||||
Mocker.GetMock<IAddArtistService>()
|
||||
.Verify(v => v.AddArtists(It.Is<List<Artist>>(t => t.Count == 1 && t.First().AddOptions.AlbumsToMonitor.Contains("09474d62-17dd-3a4f-98fb-04c65f38a479"))));
|
||||
Mocker.GetMock<IAddAlbumService>()
|
||||
.Verify(v => v.AddAlbums(It.Is<List<Album>>(t => t.Count == 0)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_mark_album_for_monitor_if_album_id_and_monitor_all_selected()
|
||||
public void should_not_add_album_if_excluded_artist()
|
||||
{
|
||||
WithArtistId();
|
||||
WithAlbum();
|
||||
WithAlbumId();
|
||||
WithMonitorType(ImportListMonitorType.EntireArtist);
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
|
||||
Mocker.GetMock<IAddArtistService>()
|
||||
.Verify(v => v.AddArtists(It.Is<List<Artist>>(t => t.Count == 1 && !t.First().AddOptions.AlbumsToMonitor.Any())));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_mark_album_for_monitor_if_no_album_id()
|
||||
{
|
||||
WithArtistId();
|
||||
WithExcludedArtist();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
|
||||
Mocker.GetMock<IAddArtistService>()
|
||||
.Verify(v => v.AddArtists(It.Is<List<Artist>>(t => t.Count == 1 && t.First().AddOptions.AlbumsToMonitor.Count == 0)));
|
||||
Mocker.GetMock<IAddAlbumService>()
|
||||
.Verify(v => v.AddAlbums(It.Is<List<Album>>(t => t.Count == 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
namespace NzbDrone.Core.Test.MetadataSource.SkyHook
|
||||
{
|
||||
[TestFixture]
|
||||
[IntegrationTest]
|
||||
public class SkyHookProxyFixture : CoreTest<SkyHookProxy>
|
||||
{
|
||||
private MetadataProfile _metadataProfile;
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
using NzbDrone.Core.MetadataSource.SkyHook;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
using NzbDrone.Test.Common.Categories;
|
||||
using Moq;
|
||||
using NzbDrone.Core.Profiles.Metadata;
|
||||
using NzbDrone.Core.Music;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Test.MetadataSource.SkyHook
|
||||
{
|
||||
[TestFixture]
|
||||
[IntegrationTest]
|
||||
public class SkyHookProxySearchFixture : CoreTest<SkyHookProxy>
|
||||
{
|
||||
[SetUp]
|
||||
@@ -107,5 +106,28 @@ public void no_artist_search_result(string term)
|
||||
|
||||
ExceptionVerification.IgnoreWarns();
|
||||
}
|
||||
|
||||
[TestCase("Eminem", 0, typeof(Artist), "Eminem")]
|
||||
[TestCase("Eminem Kamikaze", 0, typeof(Artist), "Eminem")]
|
||||
[TestCase("Eminem Kamikaze", 1, typeof(Album), "Kamikaze")]
|
||||
public void successful_combined_search(string query, int position, Type resultType, string expected)
|
||||
{
|
||||
var result = Subject.SearchForNewEntity(query);
|
||||
result.Should().NotBeEmpty();
|
||||
result[position].GetType().Should().Be(resultType);
|
||||
|
||||
if (resultType == typeof(Artist))
|
||||
{
|
||||
var cast = result[position] as Artist;
|
||||
cast.Should().NotBeNull();
|
||||
cast.Name.Should().Be(expected);
|
||||
}
|
||||
else
|
||||
{
|
||||
var cast = result[position] as Album;
|
||||
cast.Should().NotBeNull();
|
||||
cast.Title.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using FluentValidation;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Test.MusicTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class AddAlbumFixture : CoreTest<AddAlbumService>
|
||||
{
|
||||
private Artist _fakeArtist;
|
||||
private Album _fakeAlbum;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_fakeAlbum = Builder<Album>
|
||||
.CreateNew()
|
||||
.Build();
|
||||
|
||||
_fakeArtist = Builder<Artist>
|
||||
.CreateNew()
|
||||
.With(s => s.Path = null)
|
||||
.With(s => s.Metadata = Builder<ArtistMetadata>.CreateNew().Build())
|
||||
.Build();
|
||||
}
|
||||
|
||||
private void GivenValidAlbum(string lidarrId)
|
||||
{
|
||||
Mocker.GetMock<IProvideAlbumInfo>()
|
||||
.Setup(s => s.GetAlbumInfo(lidarrId))
|
||||
.Returns(Tuple.Create(_fakeArtist.Metadata.Value.ForeignArtistId,
|
||||
_fakeAlbum,
|
||||
new List<ArtistMetadata> { _fakeArtist.Metadata.Value }));
|
||||
|
||||
Mocker.GetMock<IAddArtistService>()
|
||||
.Setup(s => s.AddArtist(It.IsAny<Artist>(), It.IsAny<bool>()))
|
||||
.Returns(_fakeArtist);
|
||||
}
|
||||
|
||||
private void GivenValidPath()
|
||||
{
|
||||
Mocker.GetMock<IBuildFileNames>()
|
||||
.Setup(s => s.GetArtistFolder(It.IsAny<Artist>(), null))
|
||||
.Returns<Artist, NamingConfig>((c, n) => c.Name);
|
||||
}
|
||||
|
||||
private Album AlbumToAdd(string albumId, string artistId)
|
||||
{
|
||||
return new Album
|
||||
{
|
||||
ForeignAlbumId = albumId,
|
||||
ArtistMetadata = new ArtistMetadata
|
||||
{
|
||||
ForeignArtistId = artistId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_able_to_add_a_album_without_passing_in_name()
|
||||
{
|
||||
var newAlbum = AlbumToAdd("5537624c-3d2f-4f5c-8099-df916082c85c", "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493");
|
||||
|
||||
GivenValidAlbum(newAlbum.ForeignAlbumId);
|
||||
GivenValidPath();
|
||||
|
||||
var album = Subject.AddAlbum(newAlbum);
|
||||
|
||||
album.Title.Should().Be(_fakeAlbum.Title);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_if_album_cannot_be_found()
|
||||
{
|
||||
var newAlbum = AlbumToAdd("5537624c-3d2f-4f5c-8099-df916082c85c", "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493");
|
||||
|
||||
Mocker.GetMock<IProvideAlbumInfo>()
|
||||
.Setup(s => s.GetAlbumInfo(newAlbum.ForeignAlbumId))
|
||||
.Throws(new AlbumNotFoundException(newAlbum.ForeignAlbumId));
|
||||
|
||||
Assert.Throws<ValidationException>(() => Subject.AddAlbum(newAlbum));
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using FizzWare.NBuilder;
|
||||
|
||||
@@ -172,7 +172,7 @@ public void should_remove_album_with_no_valid_releases()
|
||||
Subject.RefreshAlbumInfo(album, null, false);
|
||||
|
||||
Mocker.GetMock<IAlbumService>()
|
||||
.Verify(x => x.DeleteAlbum(album.Id, true),
|
||||
.Verify(x => x.DeleteAlbum(album.Id, true, false),
|
||||
Times.Once());
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -14,6 +12,7 @@
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Music.Events;
|
||||
using NzbDrone.Core.ImportLists.Exclusions;
|
||||
|
||||
namespace NzbDrone.Core.Test.MusicTests
|
||||
{
|
||||
@@ -24,6 +23,7 @@ public class RefreshArtistServiceFixture : CoreTest<RefreshArtistService>
|
||||
private Album _album1;
|
||||
private Album _album2;
|
||||
private List<Album> _albums;
|
||||
private List<Album> _remoteAlbums;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
@@ -38,6 +38,9 @@ public void Setup()
|
||||
|
||||
_albums = new List<Album> {_album1, _album2};
|
||||
|
||||
_remoteAlbums = _albums.JsonClone();
|
||||
_remoteAlbums.ForEach(x => x.Id = 0);
|
||||
|
||||
var metadata = Builder<ArtistMetadata>.CreateNew().Build();
|
||||
|
||||
_artist = Builder<Artist>.CreateNew()
|
||||
@@ -62,6 +65,10 @@ public void Setup()
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(x => x.GetByArtist(It.IsAny<int>(), It.IsAny<HistoryEventType?>()))
|
||||
.Returns(new List<History.History>());
|
||||
|
||||
Mocker.GetMock<IImportListExclusionService>()
|
||||
.Setup(x => x.FindByForeignId(It.IsAny<List<string>>()))
|
||||
.Returns(new List<ImportListExclusion>());
|
||||
}
|
||||
|
||||
private void GivenNewArtistInfo(Artist artist)
|
||||
@@ -78,11 +85,11 @@ private void GivenArtistFiles()
|
||||
.Returns(Builder<TrackFile>.CreateListOfSize(1).BuildList());
|
||||
}
|
||||
|
||||
private void GivenAlbumsForRefresh()
|
||||
private void GivenAlbumsForRefresh(List<Album> albums)
|
||||
{
|
||||
Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
|
||||
.Setup(s => s.GetAlbumsForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
|
||||
.Returns(new List<Album>());
|
||||
.Returns(albums);
|
||||
}
|
||||
|
||||
private void AllowArtistUpdate()
|
||||
@@ -97,10 +104,10 @@ public void should_not_publish_artist_updated_event_if_metadata_not_updated()
|
||||
{
|
||||
var newArtistInfo = _artist.JsonClone();
|
||||
newArtistInfo.Metadata = _artist.Metadata.Value.JsonClone();
|
||||
newArtistInfo.Albums = _albums;
|
||||
newArtistInfo.Albums = _remoteAlbums;
|
||||
|
||||
GivenNewArtistInfo(newArtistInfo);
|
||||
GivenAlbumsForRefresh();
|
||||
GivenAlbumsForRefresh(_albums);
|
||||
AllowArtistUpdate();
|
||||
|
||||
Subject.Execute(new RefreshArtistCommand(_artist.Id));
|
||||
@@ -117,10 +124,10 @@ public void should_publish_artist_updated_event_if_metadata_updated()
|
||||
newArtistInfo.Metadata.Value.Images = new List<MediaCover.MediaCover> {
|
||||
new MediaCover.MediaCover(MediaCover.MediaCoverTypes.Logo, "dummy")
|
||||
};
|
||||
newArtistInfo.Albums = _albums;
|
||||
newArtistInfo.Albums = _remoteAlbums;
|
||||
|
||||
GivenNewArtistInfo(newArtistInfo);
|
||||
GivenAlbumsForRefresh();
|
||||
GivenAlbumsForRefresh(new List<Album>());
|
||||
AllowArtistUpdate();
|
||||
|
||||
Subject.Execute(new RefreshArtistCommand(_artist.Id));
|
||||
@@ -151,7 +158,7 @@ public void should_log_error_and_delete_if_musicbrainz_id_not_found_and_artist_h
|
||||
public void should_log_error_but_not_delete_if_musicbrainz_id_not_found_and_artist_has_files()
|
||||
{
|
||||
GivenArtistFiles();
|
||||
GivenAlbumsForRefresh();
|
||||
GivenAlbumsForRefresh(new List<Album>());
|
||||
|
||||
Subject.Execute(new RefreshArtistCommand(_artist.Id));
|
||||
|
||||
@@ -169,7 +176,7 @@ public void should_update_if_musicbrainz_id_changed_and_no_clash()
|
||||
{
|
||||
var newArtistInfo = _artist.JsonClone();
|
||||
newArtistInfo.Metadata = _artist.Metadata.Value.JsonClone();
|
||||
newArtistInfo.Albums = _albums;
|
||||
newArtistInfo.Albums = _remoteAlbums;
|
||||
newArtistInfo.ForeignArtistId = _artist.ForeignArtistId + 1;
|
||||
newArtistInfo.Metadata.Value.Id = 100;
|
||||
|
||||
@@ -222,8 +229,7 @@ public void should_merge_if_musicbrainz_id_changed_and_new_id_already_exists()
|
||||
|
||||
var newArtistInfo = clash.JsonClone();
|
||||
newArtistInfo.Metadata = clash.Metadata.Value.JsonClone();
|
||||
newArtistInfo.Albums = _albums.JsonClone();
|
||||
newArtistInfo.Albums.Value.ForEach(x => x.Id = 0);
|
||||
newArtistInfo.Albums = _remoteAlbums;
|
||||
|
||||
GivenNewArtistInfo(newArtistInfo);
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.HeadphonesImport;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Profiles.Metadata;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Test.Profiles.Metadata
|
||||
{
|
||||
@@ -21,7 +22,7 @@ public void init_should_add_default_profiles()
|
||||
Subject.Handle(new ApplicationStartedEvent());
|
||||
|
||||
Mocker.GetMock<IMetadataProfileRepository>()
|
||||
.Verify(v => v.Insert(It.IsAny<MetadataProfile>()), Times.Once());
|
||||
.Verify(v => v.Insert(It.IsAny<MetadataProfile>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -36,9 +37,72 @@ public void Init_should_skip_if_any_profiles_already_exist()
|
||||
Subject.Handle(new ApplicationStartedEvent());
|
||||
|
||||
Mocker.GetMock<IMetadataProfileRepository>()
|
||||
.Verify(v => v.Insert(It.IsAny<MetadataProfile>()), Times.Never());
|
||||
.Verify(v => v.Insert(It.Is<MetadataProfile>(x => x.Name == "Standard")), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void init_should_add_none_profile_if_it_doesnt_exist()
|
||||
{
|
||||
Mocker.GetMock<IMetadataProfileRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(Builder<MetadataProfile>.CreateListOfSize(2).Build().ToList());
|
||||
|
||||
Subject.Handle(new ApplicationStartedEvent());
|
||||
|
||||
Mocker.GetMock<IMetadataProfileRepository>()
|
||||
.Verify(v => v.Insert(It.Is<MetadataProfile>(x => x.Name == "None")), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void init_should_move_existing_none_profile()
|
||||
{
|
||||
var profiles = Builder<MetadataProfile>.CreateListOfSize(2)
|
||||
.TheFirst(1)
|
||||
.With(x => x.Name = MetadataProfileService.NONE_PROFILE_NAME)
|
||||
.With(x => x.PrimaryAlbumTypes = new List<ProfilePrimaryAlbumTypeItem>
|
||||
{
|
||||
new ProfilePrimaryAlbumTypeItem
|
||||
{
|
||||
PrimaryAlbumType = PrimaryAlbumType.Album,
|
||||
Allowed = true
|
||||
}
|
||||
})
|
||||
.BuildList();
|
||||
|
||||
Mocker.GetMock<IMetadataProfileRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(profiles);
|
||||
|
||||
Subject.Handle(new ApplicationStartedEvent());
|
||||
|
||||
Mocker.GetMock<IMetadataProfileRepository>()
|
||||
.Verify(v => v.Update(It.Is<MetadataProfile>(x => x.Id == profiles.First().Id && x.Name == "None.1")), Times.Once());
|
||||
|
||||
Mocker.GetMock<IMetadataProfileRepository>()
|
||||
.Verify(v => v.Insert(It.Is<MetadataProfile>(x => x.Name == "None")), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_able_to_edit_none_profile()
|
||||
{
|
||||
var profile = Builder<MetadataProfile>.CreateNew()
|
||||
.With(p => p.Name = MetadataProfileService.NONE_PROFILE_NAME)
|
||||
.Build();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => Subject.Update(profile));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_able_to_delete_none_profile()
|
||||
{
|
||||
var profile = Builder<MetadataProfile>.CreateNew()
|
||||
.With(p => p.Name = MetadataProfileService.NONE_PROFILE_NAME)
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IMetadataProfileRepository>().Setup(c => c.Get(profile.Id)).Returns(profile);
|
||||
|
||||
Assert.Throws<MetadataProfileInUseException>(() => Subject.Delete(profile.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_able_to_delete_profile_if_assigned_to_artist()
|
||||
@@ -94,10 +158,13 @@ public void should_not_be_able_to_delete_profile_if_assigned_to_import_list()
|
||||
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void should_delete_profile_if_not_assigned_to_artist_or_import_list()
|
||||
{
|
||||
var profile = Builder<MetadataProfile>.CreateNew()
|
||||
.With(p => p.Id = 1)
|
||||
.Build();
|
||||
|
||||
var artistList = Builder<Artist>.CreateListOfSize(3)
|
||||
.All()
|
||||
.With(c => c.MetadataProfileId = 2)
|
||||
@@ -110,6 +177,7 @@ public void should_delete_profile_if_not_assigned_to_artist_or_import_list()
|
||||
|
||||
Mocker.GetMock<IArtistService>().Setup(c => c.GetAllArtists()).Returns(artistList);
|
||||
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importLists);
|
||||
Mocker.GetMock<IMetadataProfileRepository>().Setup(c => c.Get(profile.Id)).Returns(profile);
|
||||
|
||||
Subject.Delete(1);
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ public interface IArtistStatisticsService
|
||||
public class ArtistStatisticsService : IArtistStatisticsService,
|
||||
IHandle<ArtistUpdatedEvent>,
|
||||
IHandle<ArtistDeletedEvent>,
|
||||
IHandle<AlbumAddedEvent>,
|
||||
IHandle<AlbumDeletedEvent>,
|
||||
IHandle<AlbumImportedEvent>,
|
||||
IHandle<AlbumEditedEvent>,
|
||||
IHandle<TrackFileDeletedEvent>
|
||||
@@ -77,6 +79,20 @@ public void Handle(ArtistDeletedEvent message)
|
||||
_cache.Remove(message.Artist.Id.ToString());
|
||||
}
|
||||
|
||||
[EventHandleOrder(EventHandleOrder.First)]
|
||||
public void Handle(AlbumAddedEvent message)
|
||||
{
|
||||
_cache.Remove("AllArtists");
|
||||
_cache.Remove(message.Album.ArtistId.ToString());
|
||||
}
|
||||
|
||||
[EventHandleOrder(EventHandleOrder.First)]
|
||||
public void Handle(AlbumDeletedEvent message)
|
||||
{
|
||||
_cache.Remove("AllArtists");
|
||||
_cache.Remove(message.Album.ArtistId.ToString());
|
||||
}
|
||||
|
||||
[EventHandleOrder(EventHandleOrder.First)]
|
||||
public void Handle(AlbumImportedEvent message)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Exclusions
|
||||
@@ -7,6 +8,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions
|
||||
public interface IImportListExclusionRepository : IBasicRepository<ImportListExclusion>
|
||||
{
|
||||
ImportListExclusion FindByForeignId(string foreignId);
|
||||
List<ImportListExclusion> FindByForeignId(List<string> ids);
|
||||
}
|
||||
|
||||
public class ImportListExclusionRepository : BasicRepository<ImportListExclusion>, IImportListExclusionRepository
|
||||
@@ -20,5 +22,10 @@ public ImportListExclusion FindByForeignId(string foreignId)
|
||||
{
|
||||
return Query.Where<ImportListExclusion>(m => m.ForeignId == foreignId).SingleOrDefault();
|
||||
}
|
||||
|
||||
public List<ImportListExclusion> FindByForeignId(List<string> ids)
|
||||
{
|
||||
return Query.Where($"[ForeignId] IN ('{string.Join("', '", ids)}')").ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,16 @@ public interface IImportListExclusionService
|
||||
ImportListExclusion Add(ImportListExclusion importListExclusion);
|
||||
List<ImportListExclusion> All();
|
||||
void Delete(int id);
|
||||
void Delete(string foreignId);
|
||||
ImportListExclusion Get(int id);
|
||||
ImportListExclusion FindByForeignId(string foreignId);
|
||||
List<ImportListExclusion> FindByForeignId(List<string> foreignIds);
|
||||
ImportListExclusion Update(ImportListExclusion importListExclusion);
|
||||
}
|
||||
|
||||
public class ImportListExclusionService : IImportListExclusionService, IHandleAsync<ArtistDeletedEvent>
|
||||
public class ImportListExclusionService : IImportListExclusionService,
|
||||
IHandleAsync<ArtistDeletedEvent>,
|
||||
IHandleAsync<AlbumDeletedEvent>
|
||||
{
|
||||
private readonly IImportListExclusionRepository _repo;
|
||||
|
||||
@@ -39,6 +43,15 @@ public void Delete(int id)
|
||||
_repo.Delete(id);
|
||||
}
|
||||
|
||||
public void Delete(string foreignId)
|
||||
{
|
||||
var exclusion = FindByForeignId(foreignId);
|
||||
if (exclusion != null)
|
||||
{
|
||||
Delete(exclusion.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public ImportListExclusion Get(int id)
|
||||
{
|
||||
return _repo.Get(id);
|
||||
@@ -49,6 +62,11 @@ public ImportListExclusion FindByForeignId(string foreignId)
|
||||
return _repo.FindByForeignId(foreignId);
|
||||
}
|
||||
|
||||
public List<ImportListExclusion> FindByForeignId(List<string> foreignIds)
|
||||
{
|
||||
return _repo.FindByForeignId(foreignIds);
|
||||
}
|
||||
|
||||
public List<ImportListExclusion> All()
|
||||
{
|
||||
return _repo.All().ToList();
|
||||
@@ -76,5 +94,28 @@ public void HandleAsync(ArtistDeletedEvent message)
|
||||
|
||||
_repo.Insert(importExclusion);
|
||||
}
|
||||
|
||||
public void HandleAsync(AlbumDeletedEvent message)
|
||||
{
|
||||
if (!message.AddImportListExclusion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existingExclusion = _repo.FindByForeignId(message.Album.ForeignAlbumId);
|
||||
|
||||
if (existingExclusion != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var importExclusion = new ImportListExclusion
|
||||
{
|
||||
ForeignId = message.Album.ForeignAlbumId,
|
||||
Name = $"{message.Album.ArtistMetadata.Value.Name} - {message.Album.Title}"
|
||||
};
|
||||
|
||||
_repo.Insert(importExclusion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,19 +20,23 @@ public class ImportListSyncService : IExecute<ImportListSyncCommand>
|
||||
private readonly ISearchForNewAlbum _albumSearchService;
|
||||
private readonly ISearchForNewArtist _artistSearchService;
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IAlbumService _albumService;
|
||||
private readonly IAddArtistService _addArtistService;
|
||||
private readonly IAddAlbumService _addAlbumService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ImportListSyncService(IImportListFactory importListFactory,
|
||||
IImportListExclusionService importListExclusionService,
|
||||
IFetchAndParseImportList listFetcherAndParser,
|
||||
ISearchForNewAlbum albumSearchService,
|
||||
ISearchForNewArtist artistSearchService,
|
||||
IArtistService artistService,
|
||||
IAddArtistService addArtistService,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
IImportListExclusionService importListExclusionService,
|
||||
IFetchAndParseImportList listFetcherAndParser,
|
||||
ISearchForNewAlbum albumSearchService,
|
||||
ISearchForNewArtist artistSearchService,
|
||||
IArtistService artistService,
|
||||
IAlbumService albumService,
|
||||
IAddArtistService addArtistService,
|
||||
IAddAlbumService addAlbumService,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
_importListFactory = importListFactory;
|
||||
_importListExclusionService = importListExclusionService;
|
||||
@@ -40,7 +44,9 @@ public class ImportListSyncService : IExecute<ImportListSyncCommand>
|
||||
_albumSearchService = albumSearchService;
|
||||
_artistSearchService = artistSearchService;
|
||||
_artistService = artistService;
|
||||
_albumService = albumService;
|
||||
_addArtistService = addArtistService;
|
||||
_addAlbumService = addAlbumService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -74,6 +80,7 @@ private List<Album> ProcessReports(List<ImportListItemInfo> reports)
|
||||
{
|
||||
var processed = new List<Album>();
|
||||
var artistsToAdd = new List<Artist>();
|
||||
var albumsToAdd = new List<Album>();
|
||||
|
||||
_logger.ProgressInfo("Processing {0} list items", reports.Count);
|
||||
|
||||
@@ -89,68 +96,156 @@ private List<Album> ProcessReports(List<ImportListItemInfo> reports)
|
||||
|
||||
var importList = _importListFactory.Get(report.ImportListId);
|
||||
|
||||
// Map MBid if we only have an album title
|
||||
if (report.AlbumMusicBrainzId.IsNullOrWhiteSpace() && report.Album.IsNotNullOrWhiteSpace())
|
||||
if (report.Album.IsNotNullOrWhiteSpace() || report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var mappedAlbum = _albumSearchService.SearchForNewAlbum(report.Album, report.Artist)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (mappedAlbum == null) continue; // Break if we are looking for an album and cant find it. This will avoid us from adding the artist and possibly getting it wrong.
|
||||
|
||||
report.AlbumMusicBrainzId = mappedAlbum.ForeignAlbumId;
|
||||
report.Album = mappedAlbum.Title;
|
||||
report.Artist = mappedAlbum.ArtistMetadata?.Value?.Name;
|
||||
report.ArtistMusicBrainzId = mappedAlbum?.ArtistMetadata?.Value?.ForeignArtistId;
|
||||
if (report.AlbumMusicBrainzId.IsNullOrWhiteSpace() || report.ArtistMusicBrainzId.IsNullOrWhiteSpace())
|
||||
{
|
||||
MapAlbumReport(report);
|
||||
}
|
||||
|
||||
ProcessAlbumReport(importList, report, listExclusions, albumsToAdd);
|
||||
}
|
||||
|
||||
// Map artist ID if we only have album ID
|
||||
if (report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() && report.ArtistMusicBrainzId.IsNullOrWhiteSpace())
|
||||
else if (report.Artist.IsNotNullOrWhiteSpace() || report.ArtistMusicBrainzId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var mappedAlbum = _albumSearchService.SearchForNewAlbum($"lidarr:{report.AlbumMusicBrainzId}", null)
|
||||
.FirstOrDefault();
|
||||
if (report.ArtistMusicBrainzId.IsNullOrWhiteSpace())
|
||||
{
|
||||
MapArtistReport(report);
|
||||
}
|
||||
|
||||
if (mappedAlbum == null) continue;
|
||||
|
||||
report.Artist = mappedAlbum.ArtistMetadata?.Value?.Name;
|
||||
report.ArtistMusicBrainzId = mappedAlbum?.ArtistMetadata?.Value?.ForeignArtistId;
|
||||
ProcessArtistReport(importList, report, listExclusions, artistsToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
// Map MBid if we only have a artist name
|
||||
if (report.ArtistMusicBrainzId.IsNullOrWhiteSpace() && report.Artist.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var mappedArtist = _artistSearchService.SearchForNewArtist(report.Artist)
|
||||
.FirstOrDefault();
|
||||
report.ArtistMusicBrainzId = mappedArtist?.Metadata.Value?.ForeignArtistId;
|
||||
report.Artist = mappedArtist?.Metadata.Value?.Name;
|
||||
}
|
||||
_addArtistService.AddArtists(artistsToAdd);
|
||||
_addAlbumService.AddAlbums(albumsToAdd);
|
||||
|
||||
// Check to see if artist in DB
|
||||
var existingArtist = _artistService.FindById(report.ArtistMusicBrainzId);
|
||||
var message = string.Format($"Import List Sync Completed. Items found: {reports.Count}, Artists added: {artistsToAdd.Count}, Albums added: {albumsToAdd.Count}");
|
||||
|
||||
// TODO: Rework this for albums when we can add albums seperate from Artists
|
||||
// (If list contains albums we should not break for an existing artist, we should add new albums that are not in DB)
|
||||
if (existingArtist != null)
|
||||
{
|
||||
_logger.Debug("{0} [{1}] Rejected, Artist Exists in DB", report.ArtistMusicBrainzId, report.Artist);
|
||||
continue;
|
||||
}
|
||||
_logger.ProgressInfo(message);
|
||||
|
||||
// Check to see if artist excluded
|
||||
var excludedArtist = listExclusions.Where(s => s.ForeignId == report.ArtistMusicBrainzId).SingleOrDefault();
|
||||
return processed;
|
||||
}
|
||||
|
||||
if (excludedArtist != null)
|
||||
{
|
||||
_logger.Debug("{0} [{1}] Rejected due to list exlcusion", report.ArtistMusicBrainzId, report.Artist);
|
||||
continue;
|
||||
}
|
||||
private void MapAlbumReport(ImportListItemInfo report)
|
||||
{
|
||||
var albumQuery = report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() ? $"lidarr:{report.AlbumMusicBrainzId}" : report.Album;
|
||||
var mappedAlbum = _albumSearchService.SearchForNewAlbum(albumQuery, report.Artist)
|
||||
.FirstOrDefault();
|
||||
|
||||
// Append Artist if not already in DB or already on add list
|
||||
if (artistsToAdd.All(s => s.Metadata.Value.ForeignArtistId != report.ArtistMusicBrainzId))
|
||||
{
|
||||
var monitored = importList.ShouldMonitor != ImportListMonitorType.None;
|
||||
// Break if we are looking for an album and cant find it. This will avoid us from adding the artist and possibly getting it wrong.
|
||||
if (mappedAlbum == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
artistsToAdd.Add(new Artist
|
||||
report.AlbumMusicBrainzId = mappedAlbum.ForeignAlbumId;
|
||||
report.Album = mappedAlbum.Title;
|
||||
report.Artist = mappedAlbum.ArtistMetadata?.Value?.Name;
|
||||
report.ArtistMusicBrainzId = mappedAlbum.ArtistMetadata?.Value?.ForeignArtistId;
|
||||
}
|
||||
|
||||
private void ProcessAlbumReport(ImportListDefinition importList, ImportListItemInfo report, List<ImportListExclusion> listExclusions, List<Album> albumsToAdd)
|
||||
{
|
||||
if (report.AlbumMusicBrainzId == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if album in DB
|
||||
var existingAlbum = _albumService.FindById(report.AlbumMusicBrainzId);
|
||||
|
||||
if (existingAlbum != null)
|
||||
{
|
||||
_logger.Debug("{0} [{1}] Rejected, Album Exists in DB", report.AlbumMusicBrainzId, report.Album);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if album excluded
|
||||
var excludedAlbum = listExclusions.SingleOrDefault(s => s.ForeignId == report.AlbumMusicBrainzId);
|
||||
|
||||
if (excludedAlbum != null)
|
||||
{
|
||||
_logger.Debug("{0} [{1}] Rejected due to list exlcusion", report.AlbumMusicBrainzId, report.Album);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if artist excluded
|
||||
var excludedArtist = listExclusions.SingleOrDefault(s => s.ForeignId == report.ArtistMusicBrainzId);
|
||||
|
||||
if (excludedArtist != null)
|
||||
{
|
||||
_logger.Debug("{0} [{1}] Rejected due to list exlcusion for parent artist", report.AlbumMusicBrainzId, report.Album);
|
||||
return;
|
||||
}
|
||||
|
||||
// Append Album if not already in DB or already on add list
|
||||
if (albumsToAdd.All(s => s.ForeignAlbumId != report.AlbumMusicBrainzId))
|
||||
{
|
||||
var monitored = importList.ShouldMonitor != ImportListMonitorType.None;
|
||||
|
||||
albumsToAdd.Add(new Album
|
||||
{
|
||||
ForeignAlbumId = report.AlbumMusicBrainzId,
|
||||
Monitored = monitored,
|
||||
Artist = new Artist
|
||||
{
|
||||
Monitored = monitored,
|
||||
RootFolderPath = importList.RootFolderPath,
|
||||
QualityProfileId = importList.ProfileId,
|
||||
MetadataProfileId = importList.MetadataProfileId,
|
||||
Tags = importList.Tags,
|
||||
AlbumFolder = true,
|
||||
AddOptions = new AddArtistOptions
|
||||
{
|
||||
SearchForMissingAlbums = monitored,
|
||||
Monitored = monitored,
|
||||
Monitor = monitored ? MonitorTypes.All : MonitorTypes.None
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void MapArtistReport(ImportListItemInfo report)
|
||||
{
|
||||
var mappedArtist = _artistSearchService.SearchForNewArtist(report.Artist)
|
||||
.FirstOrDefault();
|
||||
report.ArtistMusicBrainzId = mappedArtist?.Metadata.Value?.ForeignArtistId;
|
||||
report.Artist = mappedArtist?.Metadata.Value?.Name;
|
||||
}
|
||||
|
||||
private void ProcessArtistReport(ImportListDefinition importList, ImportListItemInfo report, List<ImportListExclusion> listExclusions, List<Artist> artistsToAdd)
|
||||
{
|
||||
if (report.ArtistMusicBrainzId == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if artist in DB
|
||||
var existingArtist = _artistService.FindById(report.ArtistMusicBrainzId);
|
||||
|
||||
if (existingArtist != null)
|
||||
{
|
||||
_logger.Debug("{0} [{1}] Rejected, Artist Exists in DB", report.ArtistMusicBrainzId, report.Artist);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if artist excluded
|
||||
var excludedArtist = listExclusions.Where(s => s.ForeignId == report.ArtistMusicBrainzId).SingleOrDefault();
|
||||
|
||||
if (excludedArtist != null)
|
||||
{
|
||||
_logger.Debug("{0} [{1}] Rejected due to list exlcusion", report.ArtistMusicBrainzId, report.Artist);
|
||||
return;
|
||||
}
|
||||
|
||||
// Append Artist if not already in DB or already on add list
|
||||
if (artistsToAdd.All(s => s.Metadata.Value.ForeignArtistId != report.ArtistMusicBrainzId))
|
||||
{
|
||||
var monitored = importList.ShouldMonitor != ImportListMonitorType.None;
|
||||
|
||||
artistsToAdd.Add(new Artist
|
||||
{
|
||||
Metadata = new ArtistMetadata
|
||||
{
|
||||
@@ -170,22 +265,7 @@ private List<Album> ProcessReports(List<ImportListItemInfo> reports)
|
||||
Monitor = monitored ? MonitorTypes.All : MonitorTypes.None
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add Album so we know what to monitor
|
||||
if (report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() && artistsToAdd.Any(s => s.Metadata.Value.ForeignArtistId == report.ArtistMusicBrainzId) && importList.ShouldMonitor == ImportListMonitorType.SpecificAlbum)
|
||||
{
|
||||
artistsToAdd.Find(s => s.Metadata.Value.ForeignArtistId == report.ArtistMusicBrainzId).AddOptions.AlbumsToMonitor.Add(report.AlbumMusicBrainzId);
|
||||
}
|
||||
}
|
||||
|
||||
_addArtistService.AddArtists(artistsToAdd);
|
||||
|
||||
var message = string.Format("Import List Sync Completed. Items found: {0}, Artists added: {1}", reports.Count, artistsToAdd.Count);
|
||||
|
||||
_logger.ProgressInfo(message);
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
public void Execute(ImportListSyncCommand message)
|
||||
|
||||
@@ -26,6 +26,7 @@ public interface IMapCoversToLocal
|
||||
public class MediaCoverService :
|
||||
IHandleAsync<ArtistRefreshCompleteEvent>,
|
||||
IHandleAsync<ArtistDeletedEvent>,
|
||||
IHandleAsync<AlbumDeletedEvent>,
|
||||
IMapCoversToLocal
|
||||
{
|
||||
private readonly IImageResizer _resizer;
|
||||
@@ -292,5 +293,13 @@ public void HandleAsync(ArtistDeletedEvent message)
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleAsync(AlbumDeletedEvent message)
|
||||
{
|
||||
var path = GetAlbumCoverPath(message.Album.Id);
|
||||
if (_diskProvider.FolderExists(path))
|
||||
{
|
||||
_diskProvider.DeleteFolder(path, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public interface IDeleteMediaFiles
|
||||
|
||||
public class MediaFileDeletionService : IDeleteMediaFiles,
|
||||
IHandleAsync<ArtistDeletedEvent>,
|
||||
IHandleAsync<AlbumDeletedEvent>,
|
||||
IHandle<TrackFileDeletedEvent>
|
||||
{
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
@@ -128,6 +129,18 @@ public void HandleAsync(ArtistDeletedEvent message)
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleAsync(AlbumDeletedEvent message)
|
||||
{
|
||||
if (message.DeleteFiles)
|
||||
{
|
||||
var files = _mediaFileService.GetFilesByAlbum(message.Album.Id);
|
||||
foreach (var file in files)
|
||||
{
|
||||
_recycleBinProvider.DeleteFile(file.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[EventHandleOrder(EventHandleOrder.Last)]
|
||||
public void Handle(TrackFileDeletedEvent message)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ public interface IMediaFileRepository : IBasicRepository<TrackFile>
|
||||
List<TrackFile> GetFilesWithBasePath(string path);
|
||||
TrackFile GetFileWithPath(string path);
|
||||
void DeleteFilesByAlbum(int albumId);
|
||||
void UnlinkFilesByAlbum(int albumId);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +71,13 @@ public void DeleteFilesByAlbum(int albumId)
|
||||
DeleteMany(ids);
|
||||
}
|
||||
|
||||
public void UnlinkFilesByAlbum(int albumId)
|
||||
{
|
||||
var files = DataMapper.Query<TrackFile>().Where(x => x.AlbumId == albumId).ToList();
|
||||
files.ForEach(x => x.AlbumId = 0);
|
||||
SetFields(files, f => f.AlbumId);
|
||||
}
|
||||
|
||||
public List<TrackFile> GetFilesByRelease(int releaseId)
|
||||
{
|
||||
return Query
|
||||
|
||||
@@ -160,7 +160,14 @@ public TrackFile GetFileWithPath(string path)
|
||||
|
||||
public void HandleAsync(AlbumDeletedEvent message)
|
||||
{
|
||||
_mediaFileRepository.DeleteFilesByAlbum(message.Album.Id);
|
||||
if (message.DeleteFiles)
|
||||
{
|
||||
_mediaFileRepository.DeleteFilesByAlbum(message.Album.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mediaFileRepository.UnlinkFilesByAlbum(message.Album.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public List<TrackFile> GetFilesByArtist(int artistId)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource
|
||||
{
|
||||
public interface ISearchForNewEntity
|
||||
{
|
||||
List<Object> SearchForNewEntity(string title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
|
||||
{
|
||||
public class EntityResource
|
||||
{
|
||||
public int Score { get; set; }
|
||||
public ArtistResource Artist { get; set; }
|
||||
public AlbumResource Album { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
@@ -15,7 +14,7 @@
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class SkyHookProxy : IProvideArtistInfo, ISearchForNewArtist, IProvideAlbumInfo, ISearchForNewAlbum
|
||||
public class SkyHookProxy : IProvideArtistInfo, ISearchForNewArtist, IProvideAlbumInfo, ISearchForNewAlbum, ISearchForNewEntity
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
@@ -247,6 +246,7 @@ public List<Album> SearchForNewAlbum(string title, string artist)
|
||||
.AddQueryParam("type", "album")
|
||||
.AddQueryParam("query", title.ToLower().Trim())
|
||||
.AddQueryParam("artist", artist.IsNotNullOrWhiteSpace() ? artist.ToLower().Trim() : string.Empty)
|
||||
.AddQueryParam("includeTracks", "1")
|
||||
.Build();
|
||||
|
||||
|
||||
@@ -266,6 +266,31 @@ public List<Album> SearchForNewAlbum(string title, string artist)
|
||||
}
|
||||
}
|
||||
|
||||
public List<Object> SearchForNewEntity(string title)
|
||||
{
|
||||
try
|
||||
{
|
||||
var httpRequest = _requestBuilder.GetRequestBuilder().Create()
|
||||
.SetSegment("route", "search")
|
||||
.AddQueryParam("type", "all")
|
||||
.AddQueryParam("query", title.ToLower().Trim())
|
||||
.Build();
|
||||
|
||||
var httpResponse = _httpClient.Get<List<EntityResource>>(httpRequest);
|
||||
|
||||
return httpResponse.Resource.SelectList(MapSearchResult);
|
||||
}
|
||||
catch (HttpException)
|
||||
{
|
||||
throw new SkyHookException("Search for '{0}' failed. Unable to communicate with LidarrAPI.", title);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, ex.Message);
|
||||
throw new SkyHookException("Search for '{0}' failed. Invalid response received from LidarrAPI.", title);
|
||||
}
|
||||
}
|
||||
|
||||
private Artist MapSearchResult(ArtistResource resource)
|
||||
{
|
||||
var artist = _artistService.FindById(resource.Id);
|
||||
@@ -280,20 +305,34 @@ private Artist MapSearchResult(ArtistResource resource)
|
||||
|
||||
private Album MapSearchResult(AlbumResource resource)
|
||||
{
|
||||
var album = _albumService.FindById(resource.Id) ?? MapAlbum(resource, null);
|
||||
var artists = resource.Artists.Select(MapArtistMetadata).ToDictionary(x => x.ForeignArtistId, x => x);
|
||||
|
||||
var artist = _artistService.FindById(resource.ArtistId);
|
||||
if (artist == null)
|
||||
{
|
||||
artist = new Artist();
|
||||
artist.Metadata = MapArtistMetadata(resource.Artists.Single(x => x.Id == resource.ArtistId));
|
||||
artist.Metadata = artists[resource.ArtistId];
|
||||
}
|
||||
|
||||
var album = _albumService.FindById(resource.Id) ?? MapAlbum(resource, artists);
|
||||
album.Artist = artist;
|
||||
album.ArtistMetadata = artist.Metadata;
|
||||
album.ArtistMetadata = artist.Metadata.Value;
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
private Object MapSearchResult(EntityResource resource)
|
||||
{
|
||||
if (resource.Artist != null)
|
||||
{
|
||||
return MapSearchResult(resource.Artist);
|
||||
}
|
||||
else
|
||||
{
|
||||
return MapSearchResult(resource.Album);
|
||||
}
|
||||
}
|
||||
|
||||
private static Album MapAlbum(AlbumResource resource, Dictionary<string, ArtistMetadata> artistDict)
|
||||
{
|
||||
Album album = new Album();
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
|
||||
public static class ArtistNameNormalizer
|
||||
{
|
||||
private readonly static Dictionary<string, string> PreComputedTitles = new Dictionary<string, string>
|
||||
{
|
||||
{ "281588", "a to z" },
|
||||
{ "266757", "ad trials triumph early church" },
|
||||
{ "289260", "ad bible continues"}
|
||||
};
|
||||
|
||||
public static string Normalize(string title, string mbID)
|
||||
{
|
||||
if (PreComputedTitles.ContainsKey(mbID))
|
||||
{
|
||||
return PreComputedTitles[mbID];
|
||||
}
|
||||
|
||||
return Parser.Parser.NormalizeTitle(title).ToLower();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music.Events
|
||||
{
|
||||
|
||||
@@ -10,11 +10,13 @@ public class AlbumDeletedEvent : IEvent
|
||||
{
|
||||
public Album Album { get; private set; }
|
||||
public bool DeleteFiles { get; private set; }
|
||||
public bool AddImportListExclusion { get; private set; }
|
||||
|
||||
public AlbumDeletedEvent(Album album, bool deleteFiles)
|
||||
public AlbumDeletedEvent(Album album, bool deleteFiles, bool addImportListExclusion)
|
||||
{
|
||||
Album = album;
|
||||
DeleteFiles = deleteFiles;
|
||||
AddImportListExclusion = addImportListExclusion;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Music.Events
|
||||
{
|
||||
public class AlbumUpdatedEvent : IEvent
|
||||
{
|
||||
public Album Album { get; private set; }
|
||||
|
||||
public AlbumUpdatedEvent(Album album)
|
||||
{
|
||||
Album = album;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music.Events
|
||||
{
|
||||
public class ArtistAddedEvent : IEvent
|
||||
{
|
||||
public Artist Artist { get; private set; }
|
||||
public bool DoRefresh { get; private set; }
|
||||
|
||||
public ArtistAddedEvent(Artist artist)
|
||||
public ArtistAddedEvent(Artist artist, bool doRefresh = true)
|
||||
{
|
||||
Artist = artist;
|
||||
DoRefresh = doRefresh;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Music.Commands;
|
||||
using NzbDrone.Core.Music.Events;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class AlbumAddedHandler : IHandle<AlbumAddedEvent>
|
||||
{
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
public AlbumAddedHandler(IManageCommandQueue commandQueueManager)
|
||||
{
|
||||
_commandQueueManager = commandQueueManager;
|
||||
}
|
||||
|
||||
public void Handle(AlbumAddedEvent message)
|
||||
{
|
||||
_commandQueueManager.Push(new RefreshArtistCommand(message.Album.Artist.Value.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -18,7 +18,10 @@ public ArtistAddedHandler(IManageCommandQueue commandQueueManager)
|
||||
|
||||
public void Handle(ArtistAddedEvent message)
|
||||
{
|
||||
_commandQueueManager.Push(new RefreshArtistCommand(message.Artist.Id, true));
|
||||
if (message.DoRefresh)
|
||||
{
|
||||
_commandQueueManager.Push(new RefreshArtistCommand(message.Artist.Id, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(ArtistsImportedEvent message)
|
||||
+15
-16
@@ -12,41 +12,40 @@ public class ArtistScannedHandler : IHandle<ArtistScannedEvent>,
|
||||
private readonly IAlbumMonitoredService _albumMonitoredService;
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
//private readonly IEpisodeAddedService _episodeAddedService;
|
||||
private readonly IAlbumAddedService _albumAddedService;
|
||||
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ArtistScannedHandler(IAlbumMonitoredService albumMonitoredService,
|
||||
IArtistService artistService,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
//IEpisodeAddedService episodeAddedService,
|
||||
IAlbumAddedService albumAddedService,
|
||||
Logger logger)
|
||||
{
|
||||
_albumMonitoredService = albumMonitoredService;
|
||||
_artistService = artistService;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
//_episodeAddedService = episodeAddedService;
|
||||
_albumAddedService = albumAddedService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private void HandleScanEvents(Artist artist)
|
||||
{
|
||||
if (artist.AddOptions == null)
|
||||
if (artist.AddOptions != null)
|
||||
{
|
||||
//_episodeAddedService.SearchForRecentlyAdded(series.Id);
|
||||
return;
|
||||
_logger.Info("[{0}] was recently added, performing post-add actions", artist.Name);
|
||||
_albumMonitoredService.SetAlbumMonitoredStatus(artist, artist.AddOptions);
|
||||
|
||||
if (artist.AddOptions.SearchForMissingAlbums)
|
||||
{
|
||||
_commandQueueManager.Push(new MissingAlbumSearchCommand(artist.Id));
|
||||
}
|
||||
|
||||
artist.AddOptions = null;
|
||||
_artistService.RemoveAddOptions(artist);
|
||||
}
|
||||
|
||||
_logger.Info("[{0}] was recently added, performing post-add actions", artist.Name);
|
||||
_albumMonitoredService.SetAlbumMonitoredStatus(artist, artist.AddOptions);
|
||||
|
||||
if (artist.AddOptions.SearchForMissingAlbums)
|
||||
{
|
||||
_commandQueueManager.Push(new MissingAlbumSearchCommand(artist.Id));
|
||||
}
|
||||
|
||||
artist.AddOptions = null;
|
||||
_artistService.RemoveAddOptions(artist);
|
||||
_albumAddedService.SearchForRecentlyAdded(artist.Id);
|
||||
}
|
||||
|
||||
public void Handle(ArtistScannedEvent message)
|
||||
@@ -0,0 +1,22 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class AddAlbumOptions : IEmbeddedDocument
|
||||
{
|
||||
public AddAlbumOptions()
|
||||
{
|
||||
// default in case not set in db
|
||||
AddType = AlbumAddType.Automatic;
|
||||
}
|
||||
|
||||
public AlbumAddType AddType { get; set; }
|
||||
public bool SearchForNewAlbum { get; set; }
|
||||
}
|
||||
|
||||
public enum AlbumAddType
|
||||
{
|
||||
Automatic,
|
||||
Manual
|
||||
}
|
||||
}
|
||||
-5
@@ -1,8 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class AddArtistOptions : MonitoringOptions
|
||||
@@ -12,14 +12,14 @@ public class Album : Entity<Album>
|
||||
public Album()
|
||||
{
|
||||
OldForeignAlbumIds = new List<string>();
|
||||
|
||||
Overview = string.Empty;
|
||||
Images = new List<MediaCover.MediaCover>();
|
||||
Links = new List<Links>();
|
||||
Genres = new List<string>();
|
||||
SecondaryTypes = new List<SecondaryAlbumType>();
|
||||
Ratings = new Ratings();
|
||||
Artist = new Artist();
|
||||
|
||||
AddOptions = new AddAlbumOptions();
|
||||
}
|
||||
|
||||
// These correspond to columns in the Albums table
|
||||
@@ -34,7 +34,7 @@ public Album()
|
||||
public List<MediaCover.MediaCover> Images { get; set; }
|
||||
public List<Links> Links { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public String AlbumType { get; set; }
|
||||
public string AlbumType { get; set; }
|
||||
public List<SecondaryAlbumType> SecondaryTypes { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
|
||||
@@ -46,7 +46,7 @@ public Album()
|
||||
public DateTime? LastInfoSync { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
[MemberwiseEqualityIgnore]
|
||||
public AddArtistOptions AddOptions { get; set; }
|
||||
public AddAlbumOptions AddOptions { get; set; }
|
||||
|
||||
// These are dynamically queried from other tables
|
||||
[MemberwiseEqualityIgnore]
|
||||
@@ -94,13 +94,13 @@ public override void UseDbFieldsFrom(Album other)
|
||||
AddOptions = other.AddOptions;
|
||||
}
|
||||
|
||||
public override void ApplyChanges(Album otherAlbum)
|
||||
public override void ApplyChanges(Album other)
|
||||
{
|
||||
ForeignAlbumId = otherAlbum.ForeignAlbumId;
|
||||
ProfileId = otherAlbum.ProfileId;
|
||||
AddOptions = otherAlbum.AddOptions;
|
||||
Monitored = otherAlbum.Monitored;
|
||||
AnyReleaseOk = otherAlbum.AnyReleaseOk;
|
||||
ForeignAlbumId = other.ForeignAlbumId;
|
||||
ProfileId = other.ProfileId;
|
||||
AddOptions = other.AddOptions;
|
||||
Monitored = other.Monitored;
|
||||
AnyReleaseOk = other.AnyReleaseOk;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,22 +75,20 @@ public override void UseDbFieldsFrom(Artist other)
|
||||
AddOptions = other.AddOptions;
|
||||
}
|
||||
|
||||
public override void ApplyChanges(Artist otherArtist)
|
||||
public override void ApplyChanges(Artist other)
|
||||
{
|
||||
Path = other.Path;
|
||||
QualityProfileId = other.QualityProfileId;
|
||||
QualityProfile = other.QualityProfile;
|
||||
MetadataProfileId = other.MetadataProfileId;
|
||||
MetadataProfile = other.MetadataProfile;
|
||||
|
||||
Path = otherArtist.Path;
|
||||
QualityProfileId = otherArtist.QualityProfileId;
|
||||
QualityProfile = otherArtist.QualityProfile;
|
||||
MetadataProfileId = otherArtist.MetadataProfileId;
|
||||
MetadataProfile = otherArtist.MetadataProfile;
|
||||
|
||||
Albums = otherArtist.Albums;
|
||||
Tags = otherArtist.Tags;
|
||||
AddOptions = otherArtist.AddOptions;
|
||||
RootFolderPath = otherArtist.RootFolderPath;
|
||||
Monitored = otherArtist.Monitored;
|
||||
AlbumFolder = otherArtist.AlbumFolder;
|
||||
|
||||
Albums = other.Albums;
|
||||
Tags = other.Tags;
|
||||
AddOptions = other.AddOptions;
|
||||
RootFolderPath = other.RootFolderPath;
|
||||
Monitored = other.Monitored;
|
||||
AlbumFolder = other.AlbumFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-6
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using Marr.Data.QGen;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Extensions;
|
||||
@@ -17,9 +16,9 @@ public interface IAlbumRepository : IBasicRepository<Album>
|
||||
List<Album> GetLastAlbums(IEnumerable<int> artistMetadataIds);
|
||||
List<Album> GetNextAlbums(IEnumerable<int> artistMetadataIds);
|
||||
List<Album> GetAlbumsByArtistMetadataId(int artistMetadataId);
|
||||
List<Album> GetAlbumsForRefresh(int artistId, IEnumerable<string> foreignIds);
|
||||
List<Album> GetAlbumsForRefresh(int artistMetadataId, IEnumerable<string> foreignIds);
|
||||
Album FindByTitle(int artistMetadataId, string title);
|
||||
Album FindById(string foreignId);
|
||||
Album FindById(string foreignAlbumId);
|
||||
PagingSpec<Album> AlbumsWithoutFiles(PagingSpec<Album> pagingSpec);
|
||||
PagingSpec<Album> AlbumsWhereCutoffUnmet(PagingSpec<Album> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff);
|
||||
List<Album> AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored);
|
||||
@@ -34,13 +33,11 @@ public interface IAlbumRepository : IBasicRepository<Album>
|
||||
public class AlbumRepository : BasicRepository<Album>, IAlbumRepository
|
||||
{
|
||||
private readonly IMainDatabase _database;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AlbumRepository(IMainDatabase database, IEventAggregator eventAggregator, Logger logger)
|
||||
public AlbumRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<Album> GetAlbums(int artistId)
|
||||
+1
-1
@@ -9,7 +9,7 @@ namespace NzbDrone.Core.Music
|
||||
public interface IArtistMetadataRepository : IBasicRepository<ArtistMetadata>
|
||||
{
|
||||
List<ArtistMetadata> FindById(List<string> foreignIds);
|
||||
bool UpsertMany(List<ArtistMetadata> artists);
|
||||
bool UpsertMany(List<ArtistMetadata> data);
|
||||
}
|
||||
|
||||
public class ArtistMetadataRepository : BasicRepository<ArtistMetadata>, IArtistMetadataRepository
|
||||
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user