parent
6af29da4c9
commit
8a20c0fa83
@ -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;
|
@ -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);
|
@ -0,0 +1 @@
|
|||||||
|
export const NONE = 'None';
|
@ -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);
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Music
|
namespace NzbDrone.Core.Music
|
||||||
{
|
{
|
||||||
public class AddArtistOptions : MonitoringOptions
|
public class AddArtistOptions : MonitoringOptions
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue