mirror of https://github.com/lidarr/Lidarr
New: Manual import improvements (#683)
* New: Manual import improvements - Detect and merge import with files already in library. - Allow selection of album release from Manual Import modal. - Loading indicator while fetching updated decisions * Disable release switching if user manually overrode release
This commit is contained in:
parent
61cea37f05
commit
188e0e1040
|
@ -321,6 +321,8 @@ class QueueRow extends Component {
|
|||
downloadId={downloadId}
|
||||
title={title}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
showReplaceExistingFiles={true}
|
||||
replaceExistingFiles={true}
|
||||
/>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
|
|
|
@ -229,16 +229,6 @@ class SignalRConnector extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleManualimport = (body) => {
|
||||
if (body.action === 'updated') {
|
||||
this.props.dispatchUpdateItem({
|
||||
section: 'interactiveImport',
|
||||
updateOnly: true,
|
||||
...body.resource
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleQueue = () => {
|
||||
if (this.props.isQueuePopulated) {
|
||||
this.props.dispatchFetchQueue();
|
||||
|
|
|
@ -65,6 +65,7 @@ class SelectAlbumModalContentConnector extends Component {
|
|||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
album,
|
||||
albumReleaseId: undefined,
|
||||
tracks: [],
|
||||
rejections: []
|
||||
});
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectAlbumReleaseModalContentConnector from './SelectAlbumReleaseModalContentConnector';
|
||||
|
||||
class SelectAlbumReleaseModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectAlbumReleaseModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectAlbumReleaseModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectAlbumReleaseModal;
|
|
@ -0,0 +1,18 @@
|
|||
.modalBody {
|
||||
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
flex: 1 1 auto;
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
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 Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import SelectAlbumReleaseRow from './SelectAlbumReleaseRow';
|
||||
import Alert from 'Components/Alert';
|
||||
import styles from './SelectAlbumReleaseModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'album',
|
||||
label: 'Album',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'release',
|
||||
label: 'Album Release',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class SelectAlbumReleaseModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
albums,
|
||||
onAlbumReleaseSelect,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Album Release
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<Alert>
|
||||
Overrriding a release here will <b>disable automatic release selection</b> for that album in future.
|
||||
</Alert>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
albums.map((item) => {
|
||||
return (
|
||||
<SelectAlbumReleaseRow
|
||||
key={item.album.id}
|
||||
matchedReleaseId={item.matchedReleaseId}
|
||||
columns={columns}
|
||||
onAlbumReleaseSelect={onAlbumReleaseSelect}
|
||||
{...item.album}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectAlbumReleaseModalContent.propTypes = {
|
||||
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onAlbumReleaseSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectAlbumReleaseModalContent;
|
|
@ -0,0 +1,67 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import SelectAlbumReleaseModalContent from './SelectAlbumReleaseModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectAlbumReleaseModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
// onSortPress = (sortKey, sortDirection) => {
|
||||
// this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection });
|
||||
// }
|
||||
|
||||
onAlbumReleaseSelect = (albumId, albumReleaseId) => {
|
||||
const ids = this.props.importIdsByAlbum[albumId];
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
albumReleaseId,
|
||||
disableReleaseSwitching: true,
|
||||
tracks: [],
|
||||
rejections: []
|
||||
});
|
||||
});
|
||||
|
||||
this.props.saveInteractiveImportItem({ id: ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectAlbumReleaseModalContent
|
||||
{...this.props}
|
||||
onAlbumReleaseSelect={this.onAlbumReleaseSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectAlbumReleaseModalContentConnector.propTypes = {
|
||||
importIdsByAlbum: PropTypes.object.isRequired,
|
||||
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SelectAlbumReleaseModalContentConnector);
|
|
@ -0,0 +1,3 @@
|
|||
.albumRow {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
|
||||
class SelectAlbumReleaseRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onAlbumReleaseSelect(parseInt(name), parseInt(value));
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
matchedReleaseId,
|
||||
title,
|
||||
disambiguation,
|
||||
releases,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
const extendedTitle = disambiguation ? `${title} (${disambiguation})` : title;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'album') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{extendedTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'release') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name={id.toString()}
|
||||
values={_.map(releases, (r) => ({
|
||||
key: r.id,
|
||||
value: `${r.title}` +
|
||||
`${r.disambiguation ? ' (' : ''}${titleCase(r.disambiguation)}${r.disambiguation ? ')' : ''}` +
|
||||
`, ${r.mediumCount} med, ${r.trackCount} tracks` +
|
||||
`${r.country.length > 0 ? ', ' : ''}${r.country}` +
|
||||
`${r.format ? ', [' : ''}${r.format}${r.format ? ']' : ''}` +
|
||||
`${r.monitored ? ', Monitored' : ''}`
|
||||
}))}
|
||||
value={matchedReleaseId}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</TableRow>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectAlbumReleaseRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
matchedReleaseId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string.isRequired,
|
||||
releases: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onAlbumReleaseSelect: PropTypes.func.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default SelectAlbumReleaseRow;
|
|
@ -41,17 +41,21 @@ class SelectArtistModalContentConnector extends Component {
|
|||
onArtistSelect = (artistId) => {
|
||||
const artist = _.find(this.props.items, { id: artistId });
|
||||
|
||||
this.props.ids.forEach((id) => {
|
||||
const ids = this.props.ids;
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
artist,
|
||||
album: undefined,
|
||||
albumReleaseId: undefined,
|
||||
tracks: [],
|
||||
rejections: []
|
||||
});
|
||||
this.props.saveInteractiveImportItem({ id });
|
||||
});
|
||||
|
||||
this.props.saveInteractiveImportItem({ id: ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ConfirmImportModalContentConnector from './ConfirmImportModalContentConnector';
|
||||
|
||||
class ConfirmImportModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ConfirmImportModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmImportModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ConfirmImportModal;
|
|
@ -0,0 +1,135 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
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 Alert from 'Components/Alert';
|
||||
|
||||
function formatAlbumFiles(items, album) {
|
||||
|
||||
return (
|
||||
<div key={album.id}>
|
||||
<b> {album.title} </b>
|
||||
<ul>
|
||||
{
|
||||
_.sortBy(items, 'path').map((item) => {
|
||||
return (
|
||||
<li key={item.id}>
|
||||
{item.path}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
class ConfirmImportModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
isPopulated
|
||||
} = this.props;
|
||||
|
||||
if (!isFetching && isPopulated && !items.length) {
|
||||
this.props.onModalClose();
|
||||
this.props.onConfirmImportPress();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
albums,
|
||||
items,
|
||||
onConfirmImportPress,
|
||||
onModalClose,
|
||||
isFetching,
|
||||
isPopulated
|
||||
} = this.props;
|
||||
|
||||
// don't render if nothing to do
|
||||
if (!isFetching && isPopulated && !items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
|
||||
{
|
||||
!isFetching && isPopulated &&
|
||||
<ModalHeader>
|
||||
Are you sure?
|
||||
</ModalHeader>
|
||||
}
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && isPopulated &&
|
||||
<div>
|
||||
<Alert>
|
||||
You are already have files imported for the albums listed below. If you continue, the existing files <b>will be deleted</b> and the new files imported in their place.
|
||||
|
||||
To avoid deleting existing files, press 'Cancel' and use the 'Combine with existing files' option.
|
||||
</Alert>
|
||||
|
||||
{ _.chain(items)
|
||||
.groupBy('albumId')
|
||||
.mapValues((value, key) => formatAlbumFiles(value, _.find(albums, (a) => a.id === parseInt(key))))
|
||||
.values()
|
||||
.value() }
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
{
|
||||
!isFetching && isPopulated &&
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={onConfirmImportPress}
|
||||
>
|
||||
Proceed
|
||||
</Button>
|
||||
|
||||
</ModalFooter>
|
||||
}
|
||||
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmImportModalContent.propTypes = {
|
||||
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
onConfirmImportPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ConfirmImportModalContent;
|
|
@ -0,0 +1,60 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchInteractiveImportTrackFiles, clearInteractiveImportTrackFiles } from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import ConfirmImportModalContent from './ConfirmImportModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('interactiveImport.trackFiles'),
|
||||
(trackFiles) => {
|
||||
return trackFiles;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchInteractiveImportTrackFiles,
|
||||
clearInteractiveImportTrackFiles
|
||||
};
|
||||
|
||||
class ConfirmImportModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
albums
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchInteractiveImportTrackFiles({ albumId: albums.map((x) => x.id) });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearInteractiveImportTrackFiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ConfirmImportModalContent
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmImportModalContentConnector.propTypes = {
|
||||
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchInteractiveImportTrackFiles: PropTypes.func.isRequired,
|
||||
clearInteractiveImportTrackFiles: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ConfirmImportModalContentConnector);
|
|
@ -19,13 +19,13 @@
|
|||
.centerButtons,
|
||||
.rightButtons {
|
||||
display: flex;
|
||||
flex: 1 2 25%;
|
||||
flex: 1 2 20%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.centerButtons {
|
||||
justify-content: center;
|
||||
flex: 2 1 50%;
|
||||
flex: 2 1 60%;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
|
|
|
@ -22,6 +22,8 @@ import Table from 'Components/Table/Table';
|
|||
import TableBody from 'Components/Table/TableBody';
|
||||
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
|
||||
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
|
||||
import SelectAlbumReleaseModal from 'InteractiveImport/AlbumRelease/SelectAlbumReleaseModal';
|
||||
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
|
||||
import InteractiveImportRow from './InteractiveImportRow';
|
||||
import styles from './InteractiveImportModalContent.css';
|
||||
|
||||
|
@ -80,6 +82,11 @@ const filterExistingFilesOptions = {
|
|||
NEW: 'new'
|
||||
};
|
||||
|
||||
const replaceExistingFilesOptions = {
|
||||
COMBINE: 'combine',
|
||||
DELETE: 'delete'
|
||||
};
|
||||
|
||||
class InteractiveImportModalContent extends Component {
|
||||
|
||||
//
|
||||
|
@ -95,10 +102,36 @@ class InteractiveImportModalContent extends Component {
|
|||
selectedState: {},
|
||||
invalidRowsSelected: [],
|
||||
isSelectArtistModalOpen: false,
|
||||
isSelectAlbumModalOpen: false
|
||||
isSelectAlbumModalOpen: false,
|
||||
isSelectAlbumReleaseModalOpen: false,
|
||||
albumsImported: [],
|
||||
isConfirmImportModalOpen: false,
|
||||
showClearTracks: false,
|
||||
inconsistentAlbumReleases: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedItems = _.filter(this.props.items, (x) => _.includes(selectedIds, x.id));
|
||||
const selectionHasTracks = _.some(selectedItems, (x) => x.tracks.length);
|
||||
|
||||
if (this.state.showClearTracks !== selectionHasTracks) {
|
||||
this.setState({ showClearTracks: selectionHasTracks });
|
||||
}
|
||||
|
||||
const inconsistent = _(selectedItems)
|
||||
.map((x) => ({ albumId: x.album ? x.album.id : 0, releaseId: x.albumReleaseId }))
|
||||
.groupBy('albumId')
|
||||
.mapValues((album) => _(album).groupBy((x) => x.releaseId).values().value().length)
|
||||
.values()
|
||||
.some((x) => x !== undefined && x > 1);
|
||||
|
||||
if (inconsistent !== this.state.inconsistentAlbumReleases) {
|
||||
this.setState({ inconsistentAlbumReleases: inconsistent });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
|
@ -120,20 +153,38 @@ class InteractiveImportModalContent extends Component {
|
|||
}
|
||||
|
||||
onValidRowChange = (id, isValid) => {
|
||||
this.setState((state) => {
|
||||
if (isValid) {
|
||||
return {
|
||||
invalidRowsSelected: _.without(state.invalidRowsSelected, id)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
invalidRowsSelected: [...state.invalidRowsSelected, id]
|
||||
};
|
||||
this.setState((state, props) => {
|
||||
// make sure to exclude any invalidRows that are no longer present in props
|
||||
const diff = _.difference(state.invalidRowsSelected, _.map(props.items, 'id'));
|
||||
const currentInvalid = _.difference(state.invalidRowsSelected, diff);
|
||||
const newstate = isValid ? _.without(currentInvalid, id) : _.union(currentInvalid, [id]);
|
||||
return { invalidRowsSelected: newstate };
|
||||
});
|
||||
}
|
||||
|
||||
onImportSelectedPress = () => {
|
||||
if (!this.props.replaceExistingFiles) {
|
||||
this.onConfirmImportPress();
|
||||
return;
|
||||
}
|
||||
|
||||
// potentially deleting files
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const albumsImported = _(this.props.items)
|
||||
.filter((x) => _.includes(selectedIds, x.id))
|
||||
.keyBy((x) => x.album.id)
|
||||
.map((x) => x.album)
|
||||
.value();
|
||||
|
||||
console.log(albumsImported);
|
||||
|
||||
this.setState({
|
||||
albumsImported,
|
||||
isConfirmImportModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onConfirmImportPress = () => {
|
||||
const {
|
||||
downloadId,
|
||||
showImportMode,
|
||||
|
@ -151,6 +202,10 @@ class InteractiveImportModalContent extends Component {
|
|||
this.props.onFilterExistingFilesChange(value !== filterExistingFilesOptions.ALL);
|
||||
}
|
||||
|
||||
onReplaceExistingFilesChange = (value) => {
|
||||
this.props.onReplaceExistingFilesChange(value === replaceExistingFilesOptions.DELETE);
|
||||
}
|
||||
|
||||
onImportModeChange = ({ value }) => {
|
||||
this.props.onImportModeChange(value);
|
||||
}
|
||||
|
@ -163,6 +218,10 @@ class InteractiveImportModalContent extends Component {
|
|||
this.setState({ isSelectAlbumModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectAlbumReleasePress = () => {
|
||||
this.setState({ isSelectAlbumReleaseModalOpen: true });
|
||||
}
|
||||
|
||||
onClearTrackMappingPress = () => {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
|
@ -175,6 +234,10 @@ class InteractiveImportModalContent extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
onGetTrackMappingPress = () => {
|
||||
this.props.saveInteractiveImportItem({ id: this.getSelectedIds() });
|
||||
}
|
||||
|
||||
onSelectArtistModalClose = () => {
|
||||
this.setState({ isSelectArtistModalOpen: false });
|
||||
}
|
||||
|
@ -183,6 +246,14 @@ class InteractiveImportModalContent extends Component {
|
|||
this.setState({ isSelectAlbumModalOpen: false });
|
||||
}
|
||||
|
||||
onSelectAlbumReleaseModalClose = () => {
|
||||
this.setState({ isSelectAlbumReleaseModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmImportModalClose = () => {
|
||||
this.setState({ isConfirmImportModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -191,12 +262,15 @@ class InteractiveImportModalContent extends Component {
|
|||
downloadId,
|
||||
allowArtistChange,
|
||||
showFilterExistingFiles,
|
||||
showReplaceExistingFiles,
|
||||
showImportMode,
|
||||
filterExistingFiles,
|
||||
replaceExistingFiles,
|
||||
title,
|
||||
folder,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
isSaving,
|
||||
error,
|
||||
items,
|
||||
sortKey,
|
||||
|
@ -213,7 +287,12 @@ class InteractiveImportModalContent extends Component {
|
|||
selectedState,
|
||||
invalidRowsSelected,
|
||||
isSelectArtistModalOpen,
|
||||
isSelectAlbumModalOpen
|
||||
isSelectAlbumModalOpen,
|
||||
isSelectAlbumReleaseModalOpen,
|
||||
albumsImported,
|
||||
isConfirmImportModalOpen,
|
||||
showClearTracks,
|
||||
inconsistentAlbumReleases
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
@ -232,43 +311,78 @@ class InteractiveImportModalContent extends Component {
|
|||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
showFilterExistingFiles &&
|
||||
<div className={styles.filterContainer}>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton>
|
||||
<Icon
|
||||
name={icons.FILTER}
|
||||
size={22}
|
||||
/>
|
||||
<div className={styles.filterContainer}>
|
||||
{
|
||||
showFilterExistingFiles &&
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton>
|
||||
<Icon
|
||||
name={icons.FILTER}
|
||||
size={22}
|
||||
/>
|
||||
|
||||
<div className={styles.filterText}>
|
||||
{
|
||||
filterExistingFiles ? 'Unmapped Files Only' : 'All Files'
|
||||
}
|
||||
</div>
|
||||
</MenuButton>
|
||||
<div className={styles.filterText}>
|
||||
{
|
||||
filterExistingFiles ? 'Unmapped Files Only' : 'All Files'
|
||||
}
|
||||
</div>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.ALL}
|
||||
isSelected={!filterExistingFiles}
|
||||
onPress={this.onFilterExistingFilesChange}
|
||||
>
|
||||
All Files
|
||||
</SelectedMenuItem>
|
||||
<MenuContent>
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.ALL}
|
||||
isSelected={!filterExistingFiles}
|
||||
onPress={this.onFilterExistingFilesChange}
|
||||
>
|
||||
All Files
|
||||
</SelectedMenuItem>
|
||||
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.NEW}
|
||||
isSelected={filterExistingFiles}
|
||||
onPress={this.onFilterExistingFilesChange}
|
||||
>
|
||||
Unmapped Files Only
|
||||
</SelectedMenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
}
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.NEW}
|
||||
isSelected={filterExistingFiles}
|
||||
onPress={this.onFilterExistingFilesChange}
|
||||
>
|
||||
Unmapped Files Only
|
||||
</SelectedMenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
}
|
||||
{
|
||||
showReplaceExistingFiles &&
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton>
|
||||
<Icon
|
||||
name={icons.CLONE}
|
||||
size={22}
|
||||
/>
|
||||
|
||||
<div className={styles.filterText}>
|
||||
{
|
||||
replaceExistingFiles ? 'Existing files will be deleted' : 'Combine with existing files'
|
||||
}
|
||||
</div>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<SelectedMenuItem
|
||||
name={replaceExistingFiles.COMBINE}
|
||||
isSelected={!replaceExistingFiles}
|
||||
onPress={this.onReplaceExistingFilesChange}
|
||||
>
|
||||
Combine With Existing Files
|
||||
</SelectedMenuItem>
|
||||
|
||||
<SelectedMenuItem
|
||||
name={replaceExistingFilesOptions.DELETE}
|
||||
isSelected={replaceExistingFiles}
|
||||
onPress={this.onReplaceExistingFilesChange}
|
||||
>
|
||||
Replace Existing Files
|
||||
</SelectedMenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
|
@ -299,6 +413,7 @@ class InteractiveImportModalContent extends Component {
|
|||
<InteractiveImportRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
isSaving={isSaving}
|
||||
{...item}
|
||||
allowArtistChange={allowArtistChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
|
@ -349,9 +464,30 @@ class InteractiveImportModalContent extends Component {
|
|||
Select Album
|
||||
</Button>
|
||||
|
||||
<Button onPress={this.onClearTrackMappingPress}>
|
||||
Clear Track Mapping
|
||||
<Button
|
||||
onPress={this.onSelectAlbumReleasePress}
|
||||
isDisabled={!selectedIds.length}
|
||||
>
|
||||
Select Release
|
||||
</Button>
|
||||
|
||||
{
|
||||
showClearTracks ? (
|
||||
<Button
|
||||
onPress={this.onClearTrackMappingPress}
|
||||
isDisabled={!selectedIds.length}
|
||||
>
|
||||
Clear Tracks
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onPress={this.onGetTrackMappingPress}
|
||||
isDisabled={!selectedIds.length}
|
||||
>
|
||||
Map Tracks
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.rightButtons}>
|
||||
|
@ -366,7 +502,7 @@ class InteractiveImportModalContent extends Component {
|
|||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
isDisabled={!selectedIds.length || !!invalidRowsSelected.length}
|
||||
isDisabled={!selectedIds.length || !!invalidRowsSelected.length || inconsistentAlbumReleases}
|
||||
onPress={this.onImportSelectedPress}
|
||||
>
|
||||
Import
|
||||
|
@ -387,6 +523,20 @@ class InteractiveImportModalContent extends Component {
|
|||
onModalClose={this.onSelectAlbumModalClose}
|
||||
/>
|
||||
|
||||
<SelectAlbumReleaseModal
|
||||
isOpen={isSelectAlbumReleaseModalOpen}
|
||||
importIdsByAlbum={_.chain(items).filter((x) => x.album).groupBy((x) => x.album.id).mapValues((x) => x.map((y) => y.id)).value()}
|
||||
albums={_.chain(items).filter((x) => x.album).keyBy((x) => x.album.id).mapValues((x) => ({ matchedReleaseId: x.albumReleaseId, album: x.album })).values().value()}
|
||||
onModalClose={this.onSelectAlbumReleaseModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmImportModal
|
||||
isOpen={isConfirmImportModalOpen}
|
||||
albums={albumsImported}
|
||||
onModalClose={this.onConfirmImportModalClose}
|
||||
onConfirmImportPress={this.onConfirmImportPress}
|
||||
/>
|
||||
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
@ -397,12 +547,15 @@ InteractiveImportModalContent.propTypes = {
|
|||
allowArtistChange: PropTypes.bool.isRequired,
|
||||
showImportMode: PropTypes.bool.isRequired,
|
||||
showFilterExistingFiles: PropTypes.bool.isRequired,
|
||||
showReplaceExistingFiles: PropTypes.bool.isRequired,
|
||||
filterExistingFiles: PropTypes.bool.isRequired,
|
||||
replaceExistingFiles: PropTypes.bool.isRequired,
|
||||
importMode: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
folder: PropTypes.string,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
|
@ -410,8 +563,10 @@ InteractiveImportModalContent.propTypes = {
|
|||
interactiveImportErrorMessage: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterExistingFilesChange: PropTypes.func.isRequired,
|
||||
onReplaceExistingFilesChange: PropTypes.func.isRequired,
|
||||
onImportModeChange: PropTypes.func.isRequired,
|
||||
onImportSelectedPress: PropTypes.func.isRequired,
|
||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
@ -419,6 +574,7 @@ InteractiveImportModalContent.propTypes = {
|
|||
InteractiveImportModalContent.defaultProps = {
|
||||
allowArtistChange: true,
|
||||
showFilterExistingFiles: false,
|
||||
showReplaceExistingFiles: false,
|
||||
showImportMode: true,
|
||||
importMode: 'move'
|
||||
};
|
||||
|
|
|
@ -3,7 +3,14 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import {
|
||||
fetchInteractiveImportItems,
|
||||
setInteractiveImportSort,
|
||||
clearInteractiveImport,
|
||||
setInteractiveImportMode,
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
|
@ -24,6 +31,7 @@ const mapDispatchToProps = {
|
|||
setInteractiveImportMode,
|
||||
clearInteractiveImport,
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
|
@ -37,7 +45,8 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
|
||||
this.state = {
|
||||
interactiveImportErrorMessage: null,
|
||||
filterExistingFiles: true
|
||||
filterExistingFiles: props.filterExistingFiles,
|
||||
replaceExistingFiles: props.replaceExistingFiles
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -48,22 +57,26 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
} = this.props;
|
||||
|
||||
const {
|
||||
filterExistingFiles
|
||||
filterExistingFiles,
|
||||
replaceExistingFiles
|
||||
} = this.state;
|
||||
|
||||
this.props.fetchInteractiveImportItems({
|
||||
downloadId,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
filterExistingFiles,
|
||||
replaceExistingFiles
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
filterExistingFiles
|
||||
filterExistingFiles,
|
||||
replaceExistingFiles
|
||||
} = this.state;
|
||||
|
||||
if (prevState.filterExistingFiles !== filterExistingFiles) {
|
||||
if (prevState.filterExistingFiles !== filterExistingFiles ||
|
||||
prevState.replaceExistingFiles !== replaceExistingFiles) {
|
||||
const {
|
||||
downloadId,
|
||||
folder
|
||||
|
@ -72,7 +85,8 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
this.props.fetchInteractiveImportItems({
|
||||
downloadId,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
filterExistingFiles,
|
||||
replaceExistingFiles
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +106,10 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
this.setState({ filterExistingFiles });
|
||||
}
|
||||
|
||||
onReplaceExistingFilesChange = (replaceExistingFiles) => {
|
||||
this.setState({ replaceExistingFiles });
|
||||
}
|
||||
|
||||
onImportModeChange = (importMode) => {
|
||||
this.props.setInteractiveImportMode({ importMode });
|
||||
}
|
||||
|
@ -109,7 +127,8 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
albumReleaseId,
|
||||
tracks,
|
||||
quality,
|
||||
language
|
||||
language,
|
||||
disableReleaseSwitching
|
||||
} = item;
|
||||
|
||||
if (!artist) {
|
||||
|
@ -146,7 +165,8 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
trackIds: _.map(tracks, 'id'),
|
||||
quality,
|
||||
language,
|
||||
downloadId: this.props.downloadId
|
||||
downloadId: this.props.downloadId,
|
||||
disableReleaseSwitching
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -158,7 +178,8 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
this.props.executeCommand({
|
||||
name: commandNames.INTERACTIVE_IMPORT,
|
||||
files,
|
||||
importMode
|
||||
importMode,
|
||||
replaceExistingFiles: this.state.replaceExistingFiles
|
||||
});
|
||||
|
||||
this.props.onModalClose();
|
||||
|
@ -170,7 +191,8 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
render() {
|
||||
const {
|
||||
interactiveImportErrorMessage,
|
||||
filterExistingFiles
|
||||
filterExistingFiles,
|
||||
replaceExistingFiles
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
|
@ -178,8 +200,10 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
interactiveImportErrorMessage={interactiveImportErrorMessage}
|
||||
filterExistingFiles={filterExistingFiles}
|
||||
replaceExistingFiles={replaceExistingFiles}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterExistingFilesChange={this.onFilterExistingFilesChange}
|
||||
onReplaceExistingFilesChange={this.onReplaceExistingFilesChange}
|
||||
onImportModeChange={this.onImportModeChange}
|
||||
onImportSelectedPress={this.onImportSelectedPress}
|
||||
/>
|
||||
|
@ -191,6 +215,7 @@ InteractiveImportModalContentConnector.propTypes = {
|
|||
downloadId: PropTypes.string,
|
||||
folder: PropTypes.string,
|
||||
filterExistingFiles: PropTypes.bool.isRequired,
|
||||
replaceExistingFiles: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchInteractiveImportItems: PropTypes.func.isRequired,
|
||||
setInteractiveImportSort: PropTypes.func.isRequired,
|
||||
|
@ -202,7 +227,8 @@ InteractiveImportModalContentConnector.propTypes = {
|
|||
};
|
||||
|
||||
InteractiveImportModalContentConnector.defaultProps = {
|
||||
filterExistingFiles: true
|
||||
filterExistingFiles: true,
|
||||
replaceExistingFiles: false
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportModalContentConnector);
|
||||
|
|
|
@ -16,3 +16,15 @@
|
|||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loading {
|
||||
composes: loading from '~Components/Loading/LoadingIndicator.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.additionalFile {
|
||||
composes: row from '~Components/Table/TableRow.css';
|
||||
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|||
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import TrackLanguage from 'Album/TrackLanguage';
|
||||
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
|
||||
|
@ -17,6 +18,7 @@ import SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal';
|
|||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import styles from './InteractiveImportRow.css';
|
||||
|
||||
class InteractiveImportRow extends Component {
|
||||
|
@ -174,7 +176,9 @@ class InteractiveImportRow extends Component {
|
|||
size,
|
||||
rejections,
|
||||
audioTags,
|
||||
additionalFile,
|
||||
isSelected,
|
||||
isSaving,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
|
@ -196,12 +200,29 @@ class InteractiveImportRow extends Component {
|
|||
|
||||
const showArtistPlaceholder = isSelected && !artist;
|
||||
const showAlbumNumberPlaceholder = isSelected && !!artist && !album;
|
||||
const showTrackNumbersPlaceholder = isSelected && !!album && !tracks.length;
|
||||
const showTrackNumbersPlaceholder = !isSaving && isSelected && !!album && !tracks.length;
|
||||
const showTrackNumbersLoading = isSaving && !tracks.length;
|
||||
const showQualityPlaceholder = isSelected && !quality;
|
||||
const showLanguagePlaceholder = isSelected && !language;
|
||||
|
||||
const pathCellContents = (
|
||||
<div>
|
||||
{relativePath}
|
||||
</div>
|
||||
);
|
||||
|
||||
const pathCell = additionalFile ? (
|
||||
<Tooltip
|
||||
anchor={pathCellContents}
|
||||
tooltip='This file is already in your library for a release you are currently importing'
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
) : pathCellContents;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRow
|
||||
className={additionalFile ? styles.additionalFile : undefined}
|
||||
>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
|
@ -212,7 +233,7 @@ class InteractiveImportRow extends Component {
|
|||
className={styles.relativePath}
|
||||
title={relativePath}
|
||||
>
|
||||
{relativePath}
|
||||
{pathCell}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCellButton
|
||||
|
@ -237,6 +258,9 @@ class InteractiveImportRow extends Component {
|
|||
isDisabled={!artist || !album}
|
||||
onPress={this.onSelectTrackPress}
|
||||
>
|
||||
{
|
||||
showTrackNumbersLoading && <LoadingIndicator size={20} className={styles.loading} />
|
||||
}
|
||||
{
|
||||
showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
|
||||
}
|
||||
|
@ -372,7 +396,9 @@ InteractiveImportRow.propTypes = {
|
|||
size: PropTypes.number.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
audioTags: PropTypes.object.isRequired,
|
||||
additionalFile: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onValidRowChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { set, updateItem } from '../baseActions';
|
||||
import { set, updateItem, removeItem } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
|
||||
|
@ -15,7 +16,7 @@ export function createCancelSaveProviderHandler(section) {
|
|||
};
|
||||
}
|
||||
|
||||
function createSaveProviderHandler(section, url, options = {}) {
|
||||
function createSaveProviderHandler(section, url, options = {}, removeStale = false) {
|
||||
return function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isSaving: true }));
|
||||
|
||||
|
@ -50,8 +51,13 @@ function createSaveProviderHandler(section, url, options = {}) {
|
|||
if (!Array.isArray(data)) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
const toRemove = removeStale && Array.isArray(id) ? _.difference(id, _.map(data, 'id')) : [];
|
||||
|
||||
dispatch(batchActions(
|
||||
data.map((item) => updateItem({ section, ...item })).concat(
|
||||
toRemove.map((item) => removeItem({ section, id: item }))
|
||||
).concat(
|
||||
set({
|
||||
section,
|
||||
isSaving: false,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { set, update } from './baseActions';
|
|||
export const section = 'interactiveImport';
|
||||
|
||||
const albumsSection = `${section}.albums`;
|
||||
const trackFilesSection = `${section}.trackFiles`;
|
||||
|
||||
//
|
||||
// State
|
||||
|
@ -24,6 +25,7 @@ const albumsSection = `${section}.albums`;
|
|||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
isSaving: false,
|
||||
error: null,
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
|
@ -54,7 +56,16 @@ export const defaultState = {
|
|||
isPopulated: false,
|
||||
error: null,
|
||||
sortKey: 'albumTitle',
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
items: []
|
||||
},
|
||||
|
||||
trackFiles: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
sortKey: 'relataivePath',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
items: []
|
||||
}
|
||||
};
|
||||
|
@ -80,6 +91,9 @@ export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'FETCH_INTERACTIVE_IMPORT_ALBUMS'
|
|||
export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'SET_INTERACTIVE_IMPORT_ALBUMS_SORT';
|
||||
export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'CLEAR_INTERACTIVE_IMPORT_ALBUMS';
|
||||
|
||||
export const FETCH_INTERACTIVE_IMPORT_TRACKFILES = 'FETCH_INTERACTIVE_IMPORT_TRACKFILES';
|
||||
export const CLEAR_INTERACTIVE_IMPORT_TRACKFILES = 'CLEAR_INTERACTIVE_IMPORT_TRACKFILES';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
|
@ -96,6 +110,9 @@ export const fetchInteractiveImportAlbums = createThunk(FETCH_INTERACTIVE_IMPORT
|
|||
export const setInteractiveImportAlbumsSort = createAction(SET_INTERACTIVE_IMPORT_ALBUMS_SORT);
|
||||
export const clearInteractiveImportAlbums = createAction(CLEAR_INTERACTIVE_IMPORT_ALBUMS);
|
||||
|
||||
export const fetchInteractiveImportTrackFiles = createThunk(FETCH_INTERACTIVE_IMPORT_TRACKFILES);
|
||||
export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_IMPORT_TRACKFILES);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
export const actionHandlers = handleThunks({
|
||||
|
@ -135,9 +152,11 @@ export const actionHandlers = handleThunks({
|
|||
});
|
||||
},
|
||||
|
||||
[SAVE_INTERACTIVE_IMPORT_ITEM]: createSaveProviderHandler(section, '/manualimport'),
|
||||
[SAVE_INTERACTIVE_IMPORT_ITEM]: createSaveProviderHandler(section, '/manualimport', {}, true),
|
||||
|
||||
[FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler('interactiveImport.albums', '/album')
|
||||
[FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler(albumsSection, '/album'),
|
||||
|
||||
[FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile')
|
||||
});
|
||||
|
||||
//
|
||||
|
@ -205,6 +224,12 @@ export const reducers = createHandleActions({
|
|||
return updateSectionState(state, albumsSection, {
|
||||
...defaultState.albums
|
||||
});
|
||||
},
|
||||
|
||||
[CLEAR_INTERACTIVE_IMPORT_TRACKFILES]: (state) => {
|
||||
return updateSectionState(state, trackFilesSection, {
|
||||
...defaultState.trackFiles
|
||||
});
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
|
@ -19,7 +19,7 @@ export const defaultState = {
|
|||
isPopulated: false,
|
||||
error: null,
|
||||
sortKey: 'mediumNumber',
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
secondarySortKey: 'absoluteTrackNumber',
|
||||
secondarySortDirection: sortDirections.ASCENDING,
|
||||
items: [],
|
||||
|
|
|
@ -271,6 +271,7 @@ class Missing extends Component {
|
|||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
showReplaceExistingFiles={true}
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
|
|
|
@ -119,7 +119,6 @@
|
|||
<Compile Include="Parse\ParseModule.cs" />
|
||||
<Compile Include="Parse\ParseResource.cs" />
|
||||
<Compile Include="ManualImport\ManualImportModule.cs" />
|
||||
<Compile Include="ManualImport\ManualImportModuleWithSignalR.cs" />
|
||||
<Compile Include="ManualImport\ManualImportResource.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileModule.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
|
||||
|
@ -255,4 +254,4 @@
|
|||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
@ -3,38 +3,39 @@ using System.Linq;
|
|||
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Lidarr.Http.Extensions;
|
||||
using NzbDrone.SignalR;
|
||||
using NzbDrone.Core.Music;
|
||||
using NLog;
|
||||
using Nancy;
|
||||
using Lidarr.Http;
|
||||
|
||||
namespace Lidarr.Api.V1.ManualImport
|
||||
{
|
||||
public class ManualImportModule : ManualImportModuleWithSignalR
|
||||
public class ManualImportModule : LidarrRestModule<ManualImportResource>
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IAlbumService _albumService;
|
||||
private readonly IReleaseService _releaseService;
|
||||
private readonly IManualImportService _manualImportService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ManualImportModule(IManualImportService manualImportService,
|
||||
IArtistService artistService,
|
||||
IAlbumService albumService,
|
||||
IReleaseService releaseService,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
Logger logger)
|
||||
: base(manualImportService, signalRBroadcaster, logger)
|
||||
{
|
||||
_artistService = artistService;
|
||||
_albumService = albumService;
|
||||
_releaseService = releaseService;
|
||||
_manualImportService = manualImportService;
|
||||
_logger = logger;
|
||||
|
||||
GetResourceAll = GetMediaFiles;
|
||||
|
||||
Put["/"] = options =>
|
||||
{
|
||||
var resource = Request.Body.FromJson<List<ManualImportResource>>();
|
||||
UpdateImportItems(resource);
|
||||
return GetManualImportItems(resource.Select(x => x.Id)).AsResponse(HttpStatusCode.Accepted);
|
||||
return UpdateImportItems(resource).AsResponse(HttpStatusCode.Accepted);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -43,8 +44,9 @@ namespace Lidarr.Api.V1.ManualImport
|
|||
var folder = (string)Request.Query.folder;
|
||||
var downloadId = (string)Request.Query.downloadId;
|
||||
var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true);
|
||||
var replaceExistingFiles = Request.GetBooleanQueryParameter("replaceExistingFiles", true);
|
||||
|
||||
return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList();
|
||||
return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles, replaceExistingFiles).ToResource().Select(AddQualityWeight).ToList();
|
||||
}
|
||||
|
||||
private ManualImportResource AddQualityWeight(ManualImportResource item)
|
||||
|
@ -59,7 +61,7 @@ namespace Lidarr.Api.V1.ManualImport
|
|||
return item;
|
||||
}
|
||||
|
||||
private void UpdateImportItems(List<ManualImportResource> resources)
|
||||
private List<ManualImportResource> UpdateImportItems(List<ManualImportResource> resources)
|
||||
{
|
||||
var items = new List<ManualImportItem>();
|
||||
foreach (var resource in resources)
|
||||
|
@ -76,12 +78,14 @@ namespace Lidarr.Api.V1.ManualImport
|
|||
Release = resource.AlbumReleaseId == 0 ? null : _releaseService.GetRelease(resource.AlbumReleaseId),
|
||||
Quality = resource.Quality,
|
||||
Language = resource.Language,
|
||||
DownloadId = resource.DownloadId
|
||||
DownloadId = resource.DownloadId,
|
||||
AdditionalFile = resource.AdditionalFile,
|
||||
ReplaceExistingFiles = resource.ReplaceExistingFiles,
|
||||
DisableReleaseSwitching = resource.DisableReleaseSwitching
|
||||
});
|
||||
}
|
||||
|
||||
//recalculate import and broadcast
|
||||
_manualImportService.UpdateItems(items);
|
||||
return _manualImportService.UpdateItems(items).Select(x => x.ToResource()).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Lidarr.Http;
|
||||
using Lidarr.Http.Extensions;
|
||||
using NzbDrone.SignalR;
|
||||
using NLog;
|
||||
|
||||
namespace Lidarr.Api.V1.ManualImport
|
||||
{
|
||||
public abstract class ManualImportModuleWithSignalR : LidarrRestModuleWithSignalR<ManualImportResource, ManualImportItem>
|
||||
{
|
||||
protected readonly IManualImportService _manualImportService;
|
||||
protected readonly Logger _logger;
|
||||
|
||||
protected ManualImportModuleWithSignalR(IManualImportService manualImportService,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
Logger logger)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_manualImportService = manualImportService;
|
||||
_logger = logger;
|
||||
|
||||
GetResourceById = GetManualImportItem;
|
||||
}
|
||||
|
||||
protected ManualImportModuleWithSignalR(IManualImportService manualImportService,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
Logger logger,
|
||||
string resource)
|
||||
: base(signalRBroadcaster, resource)
|
||||
{
|
||||
_manualImportService = manualImportService;
|
||||
_logger = logger;
|
||||
|
||||
GetResourceById = GetManualImportItem;
|
||||
}
|
||||
|
||||
protected ManualImportResource GetManualImportItem(int id)
|
||||
{
|
||||
return _manualImportService.Find(id).ToResource();
|
||||
}
|
||||
|
||||
protected List<ManualImportResource> GetManualImportItems(IEnumerable<int> ids)
|
||||
{
|
||||
return ids.Select(x => _manualImportService.Find(x).ToResource()).ToList();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
using NzbDrone.Common.Crypto;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
@ -9,7 +8,6 @@ using Lidarr.Api.V1.Tracks;
|
|||
using Lidarr.Http.REST;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace Lidarr.Api.V1.ManualImport
|
||||
|
@ -31,6 +29,9 @@ namespace Lidarr.Api.V1.ManualImport
|
|||
public string DownloadId { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
public ParsedTrackInfo AudioTags { get; set; }
|
||||
public bool AdditionalFile { get; set; }
|
||||
public bool ReplaceExistingFiles { get; set; }
|
||||
public bool DisableReleaseSwitching { get; set; }
|
||||
}
|
||||
|
||||
public static class ManualImportResourceMapper
|
||||
|
@ -56,7 +57,10 @@ namespace Lidarr.Api.V1.ManualImport
|
|||
//QualityWeight
|
||||
DownloadId = model.DownloadId,
|
||||
Rejections = model.Rejections,
|
||||
AudioTags = model.Tags
|
||||
AudioTags = model.Tags,
|
||||
AdditionalFile = model.AdditionalFile,
|
||||
ReplaceExistingFiles = model.ReplaceExistingFiles,
|
||||
DisableReleaseSwitching = model.DisableReleaseSwitching
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -78,11 +78,21 @@ namespace Lidarr.Api.V1.TrackFiles
|
|||
|
||||
if (albumIdQuery.HasValue)
|
||||
{
|
||||
int albumId = Convert.ToInt32(albumIdQuery.Value);
|
||||
var album = _albumService.GetAlbum(albumId);
|
||||
var albumArtist = _artistService.GetArtist(album.ArtistId);
|
||||
string albumIdValue = albumIdQuery.Value.ToString();
|
||||
|
||||
return _mediaFileService.GetFilesByAlbum(album.Id).ConvertAll(f => f.ToResource(albumArtist, _upgradableSpecification));
|
||||
var albumIds = albumIdValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(e => Convert.ToInt32(e))
|
||||
.ToList();
|
||||
|
||||
var result = new List<TrackFileResource>();
|
||||
foreach (var albumId in albumIds)
|
||||
{
|
||||
var album = _albumService.GetAlbum(albumId);
|
||||
var albumArtist = _artistService.GetArtist(album.ArtistId);
|
||||
result.AddRange(_mediaFileService.GetFilesByAlbum(album.Id).ConvertAll(f => f.ToResource(albumArtist, _upgradableSpecification)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
else
|
||||
|
|
|
@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Returns(_rootFolder);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Setup(v => v.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>()))
|
||||
.Setup(v => v.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), It.IsAny<bool>()))
|
||||
.Returns(new List<ImportDecision<LocalTrack>>());
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
|
@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Never());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist), Times.Never());
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist, false), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -162,7 +162,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Once());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist), Times.Never());
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist, false), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -180,7 +180,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Once());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist), Times.Never());
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist, false), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -197,7 +197,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -220,7 +220,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
.Verify(v => v.GetFiles(It.IsAny<string>(), It.IsAny<SearchOption>()), Times.Once());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -261,7 +261,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -277,7 +277,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -296,7 +296,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -316,7 +316,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -333,7 +333,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -350,7 +350,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -369,7 +369,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _artist, false), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -387,7 +387,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||
Subject.Scan(_artist);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,7 +160,9 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||
[Test]
|
||||
public void should_not_move_existing_files()
|
||||
{
|
||||
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, false);
|
||||
var track = _approvedDecisions.First();
|
||||
track.Item.ExistingFile = true;
|
||||
Subject.Import(new List<ImportDecision<LocalTrack>> { track }, false);
|
||||
|
||||
Mocker.GetMock<IUpgradeMediaFiles>()
|
||||
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, false),
|
||||
|
@ -215,13 +217,15 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_existing_metadata_files_with_the_same_path()
|
||||
public void should_delete_existing_trackfiles_with_the_same_path()
|
||||
{
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
|
||||
.Returns(Builder<TrackFile>.CreateListOfSize(1).BuildList());
|
||||
|
||||
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, false);
|
||||
var track = _approvedDecisions.First();
|
||||
track.Item.ExistingFile = true;
|
||||
Subject.Import(new List<ImportDecision<LocalTrack>> { track }, false);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Verify(v => v.Delete(It.IsAny<TrackFile>(), DeleteMediaFileReason.ManualOverride), Times.Once());
|
||||
|
|
|
@ -122,7 +122,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
|
|||
|
||||
var local = GivenLocalAlbumRelease();
|
||||
|
||||
Subject.GetCandidatesFromFingerprint(local).ShouldBeEquivalentTo(new List<AlbumRelease>());
|
||||
Subject.GetCandidatesFromFingerprint(local, null, null, null, false).ShouldBeEquivalentTo(new List<CandidateAlbumRelease>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -133,7 +133,9 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
|
|||
var localTracks = GivenLocalTracks(tracks, release);
|
||||
var localAlbumRelease = new LocalAlbumRelease(localTracks);
|
||||
|
||||
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, release).ShouldBeEquivalentTo(new List<AlbumRelease> { release });
|
||||
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, release, false).ShouldBeEquivalentTo(
|
||||
new List<CandidateAlbumRelease> { new CandidateAlbumRelease(release) }
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -149,7 +151,9 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
|
|||
.Setup(x => x.GetReleaseByForeignReleaseId("xxx"))
|
||||
.Returns(release);
|
||||
|
||||
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, null).ShouldBeEquivalentTo(new List<AlbumRelease> { release });
|
||||
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, null, false).ShouldBeEquivalentTo(
|
||||
new List<CandidateAlbumRelease> { new CandidateAlbumRelease(release) }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
|
|||
GivenFingerprints(testcase.Fingerprints);
|
||||
}
|
||||
|
||||
var result = Subject.Identify(tracks, specifiedArtist, null, null, testcase.NewDownload, testcase.SingleRelease);
|
||||
var result = Subject.Identify(tracks, specifiedArtist, null, null, testcase.NewDownload, testcase.SingleRelease, false);
|
||||
|
||||
TestLogger.Debug($"Found releases:\n{result.Where(x => x.AlbumRelease != null).Select(x => x.AlbumRelease?.ForeignReleaseId).ToJson()}");
|
||||
|
||||
|
|
|
@ -98,19 +98,23 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
Artist = _artist,
|
||||
Quality = _quality,
|
||||
Tracks = new List<Track> { new Track() },
|
||||
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi"
|
||||
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic()
|
||||
};
|
||||
|
||||
GivenVideoFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() });
|
||||
GivenAudioFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() });
|
||||
|
||||
Mocker.GetMock<IIdentificationService>()
|
||||
.Setup(s => s.Identify(It.IsAny<List<LocalTrack>>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<AlbumRelease>(), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease) => {
|
||||
.Setup(s => s.Identify(It.IsAny<List<LocalTrack>>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<AlbumRelease>(), It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting) => {
|
||||
var ret = new LocalAlbumRelease(tracks);
|
||||
ret.AlbumRelease = _albumRelease;
|
||||
return new List<LocalAlbumRelease> { ret };
|
||||
});
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(c => c.FilterExistingFiles(It.IsAny<List<string>>(), It.IsAny<Artist>()))
|
||||
.Returns((List<string> files, Artist artist) => files);
|
||||
|
||||
GivenSpecifications(_albumpass1);
|
||||
}
|
||||
|
||||
|
@ -119,7 +123,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
Mocker.SetConstant(mocks.Select(c => c.Object));
|
||||
}
|
||||
|
||||
private void GivenVideoFiles(IEnumerable<string> videoFiles)
|
||||
private void GivenAudioFiles(IEnumerable<string> videoFiles)
|
||||
{
|
||||
_audioFiles = videoFiles.ToList();
|
||||
|
||||
|
@ -145,7 +149,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
GivenAugmentationSuccess();
|
||||
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
|
||||
|
||||
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
|
||||
Subject.GetImportDecisions(_audioFiles, new Artist(), null, null, downloadClientItem, null, false, false, false, false);
|
||||
|
||||
_albumfail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
|
||||
_albumfail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
|
||||
|
@ -162,7 +166,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
GivenAugmentationSuccess();
|
||||
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
|
||||
|
||||
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
|
||||
Subject.GetImportDecisions(_audioFiles, new Artist(), null, null, downloadClientItem, null, false, false, false, false);
|
||||
|
||||
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
|
||||
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
|
||||
|
@ -180,7 +184,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
|
||||
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
|
||||
|
||||
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
|
||||
Subject.GetImportDecisions(_audioFiles, new Artist(), null, null, downloadClientItem, null, false, false, false, false);
|
||||
|
||||
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
|
||||
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
|
||||
|
@ -196,7 +200,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
GivenSpecifications(_albumfail1);
|
||||
GivenSpecifications(_pass1);
|
||||
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
|
||||
|
||||
result.Single().Approved.Should().BeFalse();
|
||||
}
|
||||
|
@ -207,7 +211,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
GivenSpecifications(_albumpass1);
|
||||
GivenSpecifications(_fail1);
|
||||
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
|
||||
|
||||
result.Single().Approved.Should().BeFalse();
|
||||
}
|
||||
|
@ -218,7 +222,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
GivenSpecifications(_albumpass1, _albumfail1, _albumpass2, _albumpass3);
|
||||
GivenSpecifications(_pass1, _pass2, _pass3);
|
||||
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
|
||||
|
||||
result.Single().Approved.Should().BeFalse();
|
||||
}
|
||||
|
@ -229,7 +233,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
|
||||
GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
|
||||
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
|
||||
|
||||
result.Single().Approved.Should().BeFalse();
|
||||
}
|
||||
|
@ -241,7 +245,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
|
||||
GivenSpecifications(_pass1, _pass2, _pass3);
|
||||
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
|
||||
|
||||
result.Single().Approved.Should().BeTrue();
|
||||
}
|
||||
|
@ -252,7 +256,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
GivenAugmentationSuccess();
|
||||
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
|
||||
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
|
||||
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
|
||||
result.Single().Rejections.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
|
@ -265,16 +269,14 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
.Setup(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
|
||||
.Throws<TestException>();
|
||||
|
||||
_audioFiles = new List<string>
|
||||
GivenAudioFiles(new []
|
||||
{
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV",
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV",
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV"
|
||||
};
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
|
||||
});
|
||||
|
||||
GivenVideoFiles(_audioFiles);
|
||||
|
||||
Subject.GetImportDecisions(_audioFiles, _artist);
|
||||
Subject.GetImportDecisions(_audioFiles, _artist, false);
|
||||
|
||||
Mocker.GetMock<IAugmentingService>()
|
||||
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
|
||||
|
@ -287,22 +289,20 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
{
|
||||
GivenSpecifications(_pass1);
|
||||
|
||||
_audioFiles = new List<string>
|
||||
{
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV",
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV",
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV"
|
||||
};
|
||||
|
||||
GivenVideoFiles(_audioFiles);
|
||||
GivenAudioFiles(new []
|
||||
{
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
|
||||
});
|
||||
|
||||
Mocker.GetMock<IIdentificationService>()
|
||||
.Setup(s => s.Identify(It.IsAny<List<LocalTrack>>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<AlbumRelease>(), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease) => {
|
||||
.Setup(s => s.Identify(It.IsAny<List<LocalTrack>>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<AlbumRelease>(), It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting) => {
|
||||
return new List<LocalAlbumRelease> { new LocalAlbumRelease(tracks) };
|
||||
});
|
||||
|
||||
var decisions = Subject.GetImportDecisions(_audioFiles, _artist);
|
||||
var decisions = Subject.GetImportDecisions(_audioFiles, _artist, false);
|
||||
|
||||
Mocker.GetMock<IAugmentingService>()
|
||||
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
|
||||
|
@ -316,16 +316,14 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
{
|
||||
GivenSpecifications(_pass1);
|
||||
|
||||
_audioFiles = new List<string>
|
||||
GivenAudioFiles(new []
|
||||
{
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV",
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV",
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV"
|
||||
};
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
|
||||
});
|
||||
|
||||
GivenVideoFiles(_audioFiles);
|
||||
|
||||
var decisions = Subject.GetImportDecisions(_audioFiles, _artist);
|
||||
var decisions = Subject.GetImportDecisions(_audioFiles, _artist, false);
|
||||
|
||||
Mocker.GetMock<IAugmentingService>()
|
||||
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
|
||||
|
@ -341,14 +339,12 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
|
|||
.Setup(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
|
||||
.Throws<TestException>();
|
||||
|
||||
_audioFiles = new List<string>
|
||||
GivenAudioFiles(new []
|
||||
{
|
||||
"The.Office.S03E115.DVDRip.XviD-OSiTV"
|
||||
};
|
||||
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
|
||||
});
|
||||
|
||||
GivenVideoFiles(_audioFiles);
|
||||
|
||||
Subject.GetImportDecisions(_audioFiles, _artist).Should().HaveCount(1);
|
||||
Subject.GetImportDecisions(_audioFiles, _artist, false).Should().HaveCount(1);
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
CleanMediaFiles(artist, mediaFileList);
|
||||
|
||||
var decisionsStopwatch = Stopwatch.StartNew();
|
||||
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, artist);
|
||||
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, artist, false);
|
||||
decisionsStopwatch.Stop();
|
||||
_logger.Debug("Import decisions complete for: {0} [{1}]", artist, decisionsStopwatch.Elapsed);
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Music;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||
{
|
||||
public class CandidateAlbumRelease
|
||||
{
|
||||
public CandidateAlbumRelease()
|
||||
{
|
||||
}
|
||||
|
||||
public CandidateAlbumRelease(AlbumRelease release)
|
||||
{
|
||||
AlbumRelease = release;
|
||||
ExistingTracks = new List<TrackFile>();
|
||||
}
|
||||
|
||||
public AlbumRelease AlbumRelease { get; set; }
|
||||
public List<TrackFile> ExistingTracks { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
@ -16,7 +18,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
{
|
||||
public interface IIdentificationService
|
||||
{
|
||||
List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease);
|
||||
List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting);
|
||||
}
|
||||
|
||||
public class IdentificationService : IIdentificationService
|
||||
|
@ -27,7 +29,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
private readonly ITrackService _trackService;
|
||||
private readonly ITrackGroupingService _trackGroupingService;
|
||||
private readonly IFingerprintingService _fingerprintingService;
|
||||
private readonly IAudioTagService _audioTagService;
|
||||
private readonly IAugmentingService _augmentingService;
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
|
@ -37,7 +41,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
ITrackService trackService,
|
||||
ITrackGroupingService trackGroupingService,
|
||||
IFingerprintingService fingerprintingService,
|
||||
IAudioTagService audioTagService,
|
||||
IAugmentingService augmentingService,
|
||||
IMediaFileService mediaFileService,
|
||||
IConfigService configService,
|
||||
Logger logger)
|
||||
{
|
||||
|
@ -47,7 +53,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
_trackService = trackService;
|
||||
_trackGroupingService = trackGroupingService;
|
||||
_fingerprintingService = fingerprintingService;
|
||||
_audioTagService = audioTagService;
|
||||
_augmentingService = augmentingService;
|
||||
_mediaFileService = mediaFileService;
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
@ -92,10 +100,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
_logger.Debug($"*** IdentificationService TestCaseGenerator ***\n{output}");
|
||||
}
|
||||
|
||||
public List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease)
|
||||
public List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting)
|
||||
{
|
||||
// 1 group localTracks so that we think they represent a single release
|
||||
// 2 get candidates given specified artist, album and release
|
||||
// 2 get candidates given specified artist, album and release. Candidates can include extra files already on disk.
|
||||
// 3 find best candidate
|
||||
// 4 If best candidate worse than threshold, try fingerprinting
|
||||
|
||||
|
@ -126,7 +134,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
{
|
||||
_logger.Warn($"Augmentation failed for {localRelease}");
|
||||
}
|
||||
IdentifyRelease(localRelease, artist, album, release, newDownload);
|
||||
IdentifyRelease(localRelease, artist, album, release, newDownload, includeExisting);
|
||||
}
|
||||
|
||||
watch.Stop();
|
||||
|
@ -165,18 +173,33 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
return false;
|
||||
}
|
||||
|
||||
private void IdentifyRelease(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release, bool newDownload)
|
||||
private List<LocalTrack> ToLocalTrack(IEnumerable<TrackFile> trackfiles)
|
||||
{
|
||||
var localTracks = trackfiles.Select(x => new LocalTrack {
|
||||
Path = x.Path,
|
||||
FileTrackInfo = _audioTagService.ReadTags(x.Path),
|
||||
ExistingFile = true,
|
||||
AdditionalFile = true
|
||||
})
|
||||
.ToList();
|
||||
|
||||
localTracks.ForEach(x => _augmentingService.Augment(x, true));
|
||||
|
||||
return localTracks;
|
||||
}
|
||||
|
||||
private void IdentifyRelease(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release, bool newDownload, bool includeExisting)
|
||||
{
|
||||
var watch = System.Diagnostics.Stopwatch.StartNew();
|
||||
bool fingerprinted = false;
|
||||
|
||||
var candidateReleases = GetCandidatesFromTags(localAlbumRelease, artist, album, release);
|
||||
var candidateReleases = GetCandidatesFromTags(localAlbumRelease, artist, album, release, includeExisting);
|
||||
if (candidateReleases.Count == 0 && FingerprintingAllowed(newDownload))
|
||||
{
|
||||
_logger.Debug("No candidates found, fingerprinting");
|
||||
_fingerprintingService.Lookup(localAlbumRelease.LocalTracks, 0.5);
|
||||
fingerprinted = true;
|
||||
candidateReleases = GetCandidatesFromFingerprint(localAlbumRelease);
|
||||
candidateReleases = GetCandidatesFromFingerprint(localAlbumRelease, artist, album, release, includeExisting);
|
||||
}
|
||||
|
||||
if (candidateReleases.Count == 0)
|
||||
|
@ -187,11 +210,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
|
||||
_logger.Debug($"Got {candidateReleases.Count} candidates for {localAlbumRelease.LocalTracks.Count} tracks in {watch.ElapsedMilliseconds}ms");
|
||||
|
||||
var allTracks = _trackService.GetTracksByReleases(candidateReleases.Select(x => x.Id).ToList());
|
||||
var allTracks = _trackService.GetTracksByReleases(candidateReleases.Select(x => x.AlbumRelease.Id).ToList());
|
||||
|
||||
// convert all the TrackFiles that represent extra files to List<LocalTrack>
|
||||
var allLocalTracks = ToLocalTrack(candidateReleases
|
||||
.SelectMany(x => x.ExistingTracks)
|
||||
.DistinctBy(x => x.Path));
|
||||
|
||||
_logger.Debug($"Retrieved {allTracks.Count} possible tracks in {watch.ElapsedMilliseconds}ms");
|
||||
|
||||
GetBestRelease(localAlbumRelease, candidateReleases, allTracks);
|
||||
GetBestRelease(localAlbumRelease, candidateReleases, allTracks, allLocalTracks);
|
||||
|
||||
// If result isn't great and we haven't fingerprinted, try that
|
||||
// Note that this can improve the match even if we try the same candidates
|
||||
|
@ -204,12 +232,20 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
// Will generally be specified as part of manual import
|
||||
if (album == null && release == null)
|
||||
{
|
||||
var extraCandidates = GetCandidatesFromFingerprint(localAlbumRelease).DistinctBy(x => x.Id);
|
||||
candidateReleases.AddRange(extraCandidates);
|
||||
allTracks.AddRange(_trackService.GetTracksByReleases(extraCandidates.Select(x => x.Id).ToList()));
|
||||
var extraCandidates = GetCandidatesFromFingerprint(localAlbumRelease, artist, album, release, includeExisting);
|
||||
var newCandidates = extraCandidates.ExceptBy(x => x.AlbumRelease.Id, candidateReleases, y => y.AlbumRelease.Id, EqualityComparer<int>.Default);
|
||||
candidateReleases.AddRange(newCandidates);
|
||||
allTracks.AddRange(_trackService.GetTracksByReleases(newCandidates.Select(x => x.AlbumRelease.Id).ToList()));
|
||||
allLocalTracks.AddRange(ToLocalTrack(newCandidates
|
||||
.SelectMany(x => x.ExistingTracks)
|
||||
.DistinctBy(x => x.Path)
|
||||
.ExceptBy(x => x.Path, allLocalTracks, x => x.Path, PathEqualityComparer.Instance)));
|
||||
}
|
||||
|
||||
GetBestRelease(localAlbumRelease, candidateReleases, allTracks);
|
||||
// fingerprint all the local files in candidates we might be matching against
|
||||
_fingerprintingService.Lookup(allLocalTracks, 0.5);
|
||||
|
||||
GetBestRelease(localAlbumRelease, candidateReleases, allTracks, allLocalTracks);
|
||||
}
|
||||
|
||||
_logger.Debug($"Best release found in {watch.ElapsedMilliseconds}ms");
|
||||
|
@ -219,43 +255,70 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
_logger.Debug($"IdentifyRelease done in {watch.ElapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
public List<AlbumRelease> GetCandidatesFromTags(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release)
|
||||
public List<CandidateAlbumRelease> GetCandidatesFromTags(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release, bool includeExisting)
|
||||
{
|
||||
var watch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
// Generally artist, album and release are null. But if they're not then limit candidates appropriately.
|
||||
// We've tried to make sure that tracks are all for a single release.
|
||||
|
||||
List<AlbumRelease> candidateReleases;
|
||||
List<CandidateAlbumRelease> candidateReleases;
|
||||
|
||||
// if we have a release ID that makes sense, use that
|
||||
// if we have a release ID, use that
|
||||
AlbumRelease tagMbidRelease = null;
|
||||
List<CandidateAlbumRelease> tagCandidate = null;
|
||||
|
||||
var releaseIds = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.ReleaseMBId).Distinct().ToList();
|
||||
if (releaseIds.Count == 1 && releaseIds[0].IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var tagRelease = _releaseService.GetReleaseByForeignReleaseId(releaseIds[0]);
|
||||
if (tagRelease != null)
|
||||
_logger.Debug("Selecting release from consensus ForeignReleaseId [{0}]", releaseIds[0]);
|
||||
tagMbidRelease = _releaseService.GetReleaseByForeignReleaseId(releaseIds[0]);
|
||||
if (tagMbidRelease != null)
|
||||
{
|
||||
_logger.Debug("Selecting release from consensus ForeignReleaseId [{0}]", releaseIds[0]);
|
||||
return new List<AlbumRelease> { tagRelease };
|
||||
tagCandidate = GetCandidatesByRelease(new List<AlbumRelease> { tagMbidRelease }, includeExisting);
|
||||
}
|
||||
}
|
||||
|
||||
if (release != null)
|
||||
{
|
||||
// this case overrides the release picked up from the file tags
|
||||
_logger.Debug("Release {0} [{1} tracks] was forced", release, release.TrackCount);
|
||||
candidateReleases = new List<AlbumRelease> { release };
|
||||
candidateReleases = GetCandidatesByRelease(new List<AlbumRelease> { release }, includeExisting);
|
||||
}
|
||||
else if (album != null)
|
||||
{
|
||||
candidateReleases = GetCandidatesByAlbum(localAlbumRelease, album);
|
||||
// use the release from file tags if it exists and agrees with the specified album
|
||||
if (tagMbidRelease?.AlbumId == album.Id)
|
||||
{
|
||||
candidateReleases = tagCandidate;
|
||||
}
|
||||
else
|
||||
{
|
||||
candidateReleases = GetCandidatesByAlbum(localAlbumRelease, album, includeExisting);
|
||||
}
|
||||
}
|
||||
else if (artist != null)
|
||||
{
|
||||
candidateReleases = GetCandidatesByArtist(localAlbumRelease, artist);
|
||||
// use the release from file tags if it exists and agrees with the specified album
|
||||
if (tagMbidRelease?.Album.Value.ArtistMetadataId == artist.ArtistMetadataId)
|
||||
{
|
||||
candidateReleases = tagCandidate;
|
||||
}
|
||||
else
|
||||
{
|
||||
candidateReleases = GetCandidatesByArtist(localAlbumRelease, artist, includeExisting);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
candidateReleases = GetCandidates(localAlbumRelease);
|
||||
if (tagMbidRelease != null)
|
||||
{
|
||||
candidateReleases = tagCandidate;
|
||||
}
|
||||
else
|
||||
{
|
||||
candidateReleases = GetCandidates(localAlbumRelease, includeExisting);
|
||||
}
|
||||
}
|
||||
|
||||
watch.Stop();
|
||||
|
@ -265,19 +328,41 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
return candidateReleases;
|
||||
}
|
||||
|
||||
private List<AlbumRelease> GetCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album)
|
||||
private List<CandidateAlbumRelease> GetCandidatesByRelease(List<AlbumRelease> releases, bool includeExisting)
|
||||
{
|
||||
// get the local tracks on disk for each album
|
||||
var albumTracks = releases.Select(x => x.AlbumId)
|
||||
.Distinct()
|
||||
.ToDictionary(id => id, id => includeExisting ? _mediaFileService.GetFilesByAlbum(id) : new List<TrackFile>());
|
||||
|
||||
// populate the path. Artist will have been returned by mediaFileService
|
||||
foreach (var trackfiles in albumTracks.Values)
|
||||
{
|
||||
foreach (var trackfile in trackfiles)
|
||||
{
|
||||
trackfile.Path = Path.Combine(trackfile.Artist.Value.Path, trackfile.RelativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return releases.Select(x => new CandidateAlbumRelease {
|
||||
AlbumRelease = x,
|
||||
ExistingTracks = albumTracks[x.AlbumId]
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private List<CandidateAlbumRelease> GetCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album, bool includeExisting)
|
||||
{
|
||||
// sort candidate releases by closest track count so that we stand a chance of
|
||||
// getting a perfect match early on
|
||||
return _releaseService.GetReleasesByAlbum(album.Id)
|
||||
.OrderBy(x => Math.Abs(localAlbumRelease.TrackCount - x.TrackCount))
|
||||
.ToList();
|
||||
return GetCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id)
|
||||
.OrderBy(x => Math.Abs(localAlbumRelease.TrackCount - x.TrackCount))
|
||||
.ToList(), includeExisting);
|
||||
}
|
||||
|
||||
private List<AlbumRelease> GetCandidatesByArtist(LocalAlbumRelease localAlbumRelease, Artist artist)
|
||||
private List<CandidateAlbumRelease> GetCandidatesByArtist(LocalAlbumRelease localAlbumRelease, Artist artist, bool includeExisting)
|
||||
{
|
||||
_logger.Trace("Getting candidates for {0}", artist);
|
||||
var candidateReleases = new List<AlbumRelease>();
|
||||
var candidateReleases = new List<CandidateAlbumRelease>();
|
||||
|
||||
var albumTag = MostCommon(localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.AlbumTitle)) ?? "";
|
||||
if (albumTag.IsNotNullOrWhiteSpace())
|
||||
|
@ -285,14 +370,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
var possibleAlbums = _albumService.GetCandidates(artist.ArtistMetadataId, albumTag);
|
||||
foreach (var album in possibleAlbums)
|
||||
{
|
||||
candidateReleases.AddRange(GetCandidatesByAlbum(localAlbumRelease, album));
|
||||
candidateReleases.AddRange(GetCandidatesByAlbum(localAlbumRelease, album, includeExisting));
|
||||
}
|
||||
}
|
||||
|
||||
return candidateReleases;
|
||||
}
|
||||
|
||||
private List<AlbumRelease> GetCandidates(LocalAlbumRelease localAlbumRelease)
|
||||
private List<CandidateAlbumRelease> GetCandidates(LocalAlbumRelease localAlbumRelease, bool includeExisting)
|
||||
{
|
||||
// most general version, nothing has been specified.
|
||||
// get all plausible artists, then all plausible albums, then get releases for each of these.
|
||||
|
@ -303,7 +388,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
throw new NotImplementedException("Various artists not supported");
|
||||
}
|
||||
|
||||
var candidateReleases = new List<AlbumRelease>();
|
||||
var candidateReleases = new List<CandidateAlbumRelease>();
|
||||
|
||||
var artistTag = MostCommon(localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.ArtistTitle)) ?? "";
|
||||
if (artistTag.IsNotNullOrWhiteSpace())
|
||||
|
@ -311,30 +396,44 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
var possibleArtists = _artistService.GetCandidates(artistTag);
|
||||
foreach (var artist in possibleArtists)
|
||||
{
|
||||
candidateReleases.AddRange(GetCandidatesByArtist(localAlbumRelease, artist));
|
||||
candidateReleases.AddRange(GetCandidatesByArtist(localAlbumRelease, artist, includeExisting));
|
||||
}
|
||||
}
|
||||
|
||||
return candidateReleases;
|
||||
}
|
||||
|
||||
public List<AlbumRelease> GetCandidatesFromFingerprint(LocalAlbumRelease localAlbumRelease)
|
||||
public List<CandidateAlbumRelease> GetCandidatesFromFingerprint(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release, bool includeExisting)
|
||||
{
|
||||
var recordingIds = localAlbumRelease.LocalTracks.Where(x => x.AcoustIdResults != null).SelectMany(x => x.AcoustIdResults).ToList();
|
||||
var allReleases = _releaseService.GetReleasesByRecordingIds(recordingIds);
|
||||
|
||||
return allReleases.Select(x => new {
|
||||
Release = x,
|
||||
TrackCount = x.TrackCount,
|
||||
CommonProportion = x.Tracks.Value.Select(y => y.ForeignRecordingId).Intersect(recordingIds).Count() / localAlbumRelease.TrackCount
|
||||
})
|
||||
// make sure releases are consistent with those selected by the user
|
||||
if (release != null)
|
||||
{
|
||||
allReleases = allReleases.Where(x => x.Id == release.Id).ToList();
|
||||
}
|
||||
else if (album != null)
|
||||
{
|
||||
allReleases = allReleases.Where(x => x.AlbumId == album.Id).ToList();
|
||||
}
|
||||
else if (artist != null)
|
||||
{
|
||||
allReleases = allReleases.Where(x => x.Album.Value.ArtistMetadataId == artist.ArtistMetadataId).ToList();
|
||||
}
|
||||
|
||||
return GetCandidatesByRelease(allReleases.Select(x => new {
|
||||
Release = x,
|
||||
TrackCount = x.TrackCount,
|
||||
CommonProportion = x.Tracks.Value.Select(y => y.ForeignRecordingId).Intersect(recordingIds).Count() / localAlbumRelease.TrackCount
|
||||
})
|
||||
.Where(x => x.CommonProportion > 0.6)
|
||||
.ToList()
|
||||
.OrderBy(x => Math.Abs(x.TrackCount - localAlbumRelease.TrackCount))
|
||||
.ThenByDescending(x => x.CommonProportion)
|
||||
.Select(x => x.Release)
|
||||
.Take(10)
|
||||
.ToList();
|
||||
.ToList(), includeExisting);
|
||||
}
|
||||
|
||||
private T MostCommon<T>(IEnumerable<T> items)
|
||||
|
@ -342,7 +441,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
return items.GroupBy(x => x).OrderByDescending(x => x.Count()).First().Key;
|
||||
}
|
||||
|
||||
private void GetBestRelease(LocalAlbumRelease localAlbumRelease, List<AlbumRelease> candidateReleases, List<Track> tracks)
|
||||
private void GetBestRelease(LocalAlbumRelease localAlbumRelease, List<CandidateAlbumRelease> candidateReleases, List<Track> dbTracks, List<LocalTrack> extraTracksOnDisk)
|
||||
{
|
||||
var watch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
|
@ -351,13 +450,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
|
||||
double bestDistance = 1.0;
|
||||
|
||||
foreach (var release in candidateReleases)
|
||||
foreach (var candidateRelease in candidateReleases)
|
||||
{
|
||||
_logger.Debug("Trying Release {0} [{1}, {2} tracks]", release, release.Title, release.TrackCount);
|
||||
var release = candidateRelease.AlbumRelease;
|
||||
_logger.Debug("Trying Release {0} [{1}, {2} tracks, {3} existing]", release, release.Title, release.TrackCount, candidateRelease.ExistingTracks.Count);
|
||||
var rwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
var extraTrackPaths = candidateRelease.ExistingTracks.Select(x => x.Path).ToList();
|
||||
var extraTracks = extraTracksOnDisk.Where(x => extraTrackPaths.Contains(x.Path)).ToList();
|
||||
var allLocalTracks = localAlbumRelease.LocalTracks.Concat(extraTracks).DistinctBy(x => x.Path).ToList();
|
||||
|
||||
var mapping = MapReleaseTracks(localAlbumRelease.LocalTracks, tracks.Where(x => x.AlbumReleaseId == release.Id).ToList());
|
||||
var distance = AlbumReleaseDistance(localAlbumRelease.LocalTracks, release, mapping);
|
||||
var mapping = MapReleaseTracks(allLocalTracks, dbTracks.Where(x => x.AlbumReleaseId == release.Id).ToList());
|
||||
var distance = AlbumReleaseDistance(allLocalTracks, release, mapping);
|
||||
var currDistance = distance.NormalizedDistance();
|
||||
|
||||
rwatch.Stop();
|
||||
|
@ -368,6 +472,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|||
bestDistance = currDistance;
|
||||
localAlbumRelease.Distance = distance;
|
||||
localAlbumRelease.AlbumRelease = release;
|
||||
localAlbumRelease.ExistingTracks = extraTracks;
|
||||
localAlbumRelease.TrackMapping = mapping;
|
||||
if (currDistance == 0.0)
|
||||
{
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
{
|
||||
public interface IImportApprovedTracks
|
||||
{
|
||||
List<ImportResult> Import(List<ImportDecision<LocalTrack>> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto);
|
||||
List<ImportResult> Import(List<ImportDecision<LocalTrack>> decisions, bool replaceExisting, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto);
|
||||
}
|
||||
|
||||
public class ImportApprovedTracks : IImportApprovedTracks
|
||||
|
@ -58,7 +58,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<ImportResult> Import(List<ImportDecision<LocalTrack>> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto)
|
||||
public List<ImportResult> Import(List<ImportDecision<LocalTrack>> decisions, bool replaceExisting, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto)
|
||||
{
|
||||
var qualifiedImports = decisions.Where(c => c.Approved)
|
||||
.GroupBy(c => c.Item.Artist.Id, (i, s) => s
|
||||
|
@ -67,54 +67,49 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
.ThenByDescending(c => c.Item.Size))
|
||||
.SelectMany(c => c)
|
||||
.ToList();
|
||||
|
||||
_logger.Debug($"Importing {qualifiedImports.Count} files. replaceExisting: {replaceExisting}");
|
||||
|
||||
var importResults = new List<ImportResult>();
|
||||
var allImportedTrackFiles = new List<TrackFile>();
|
||||
var allOldTrackFiles = new List<TrackFile>();
|
||||
|
||||
var albumDecisions = decisions.Where(e => e.Item.Album != null)
|
||||
var albumDecisions = decisions.Where(e => e.Item.Album != null && e.Approved)
|
||||
.GroupBy(e => e.Item.Album.Id).ToList();
|
||||
|
||||
foreach (var albumDecision in albumDecisions)
|
||||
{
|
||||
var album = albumDecision.First().Item.Album;
|
||||
var currentRelease = album.AlbumReleases.Value.Single(x => x.Monitored);
|
||||
var newRelease = albumDecision.First().Item.Release;
|
||||
|
||||
if (albumDecision.Any(x => x.Approved))
|
||||
if (replaceExisting)
|
||||
{
|
||||
var newRelease = albumDecision.First(x => x.Approved).Item.Release;
|
||||
var artist = albumDecision.First().Item.Artist;
|
||||
var rootFolder = _diskProvider.GetParentFolder(artist.Path);
|
||||
var previousFiles = _mediaFileService.GetFilesByAlbum(album.Id);
|
||||
|
||||
if (currentRelease.Id != newRelease.Id)
|
||||
_logger.Debug($"Deleting {previousFiles.Count} existing files for {album}");
|
||||
|
||||
foreach (var previousFile in previousFiles)
|
||||
{
|
||||
// if we are importing a new release, delete all old files and don't attempt to upgrade
|
||||
if (newDownload)
|
||||
var trackFilePath = Path.Combine(artist.Path, previousFile.RelativePath);
|
||||
var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(trackFilePath));
|
||||
if (_diskProvider.FileExists(trackFilePath))
|
||||
{
|
||||
var artist = albumDecision.First().Item.Artist;
|
||||
var rootFolder = _diskProvider.GetParentFolder(artist.Path);
|
||||
var previousFiles = _mediaFileService.GetFilesByAlbum(album.Id);
|
||||
|
||||
foreach (var previousFile in previousFiles)
|
||||
{
|
||||
var trackFilePath = Path.Combine(artist.Path, previousFile.RelativePath);
|
||||
var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(trackFilePath));
|
||||
if (_diskProvider.FileExists(trackFilePath))
|
||||
{
|
||||
_logger.Debug("Removing existing track file: {0}", previousFile);
|
||||
_recycleBinProvider.DeleteFile(trackFilePath, subfolder);
|
||||
}
|
||||
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.Upgrade);
|
||||
}
|
||||
_logger.Debug("Removing existing track file: {0}", previousFile);
|
||||
_recycleBinProvider.DeleteFile(trackFilePath, subfolder);
|
||||
}
|
||||
|
||||
// set the correct release to be monitored before importing the new files
|
||||
_logger.Debug("Updating release to {0} [{1} tracks]", newRelease, newRelease.TrackCount);
|
||||
_releaseService.SetMonitored(newRelease);
|
||||
|
||||
// Publish album edited event.
|
||||
// Deliberatly don't put in the old album since we don't want to trigger an ArtistScan.
|
||||
_eventAggregator.PublishEvent(new AlbumEditedEvent(album, album));
|
||||
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.Upgrade);
|
||||
}
|
||||
}
|
||||
|
||||
// set the correct release to be monitored before importing the new files
|
||||
_logger.Debug("Updating release to {0} [{1} tracks]", newRelease, newRelease.TrackCount);
|
||||
_releaseService.SetMonitored(newRelease);
|
||||
|
||||
// Publish album edited event.
|
||||
// Deliberatly don't put in the old album since we don't want to trigger an ArtistScan.
|
||||
_eventAggregator.PublishEvent(new AlbumEditedEvent(album, album));
|
||||
}
|
||||
|
||||
var filesToAdd = new List<TrackFile>(qualifiedImports.Count);
|
||||
|
@ -186,7 +181,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
break;
|
||||
}
|
||||
|
||||
if (newDownload)
|
||||
if (!localTrack.ExistingFile)
|
||||
{
|
||||
trackFile.SceneName = GetSceneReleaseName(downloadClientItem, localTrack);
|
||||
|
||||
|
@ -205,13 +200,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.ManualOverride);
|
||||
}
|
||||
|
||||
_audioTagService.WriteTags(trackFile, newDownload);
|
||||
_audioTagService.WriteTags(trackFile, false);
|
||||
}
|
||||
|
||||
filesToAdd.Add(trackFile);
|
||||
importResults.Add(new ImportResult(importDecision));
|
||||
|
||||
if (newDownload)
|
||||
if (!localTrack.ExistingFile)
|
||||
{
|
||||
_extraService.ImportTrack(localTrack, trackFile, copyOnly);
|
||||
}
|
||||
|
@ -219,12 +214,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
allImportedTrackFiles.Add(trackFile);
|
||||
allOldTrackFiles.AddRange(oldFiles);
|
||||
|
||||
_eventAggregator.PublishEvent(new TrackImportedEvent(localTrack, trackFile, oldFiles, newDownload, downloadClientItem));
|
||||
_eventAggregator.PublishEvent(new TrackImportedEvent(localTrack, trackFile, oldFiles, !localTrack.ExistingFile, downloadClientItem));
|
||||
}
|
||||
catch (RootFolderNotFoundException e)
|
||||
{
|
||||
_logger.Warn(e, "Couldn't import track " + localTrack);
|
||||
_eventAggregator.PublishEvent(new TrackImportFailedEvent(e, localTrack, newDownload, downloadClientItem));
|
||||
_eventAggregator.PublishEvent(new TrackImportFailedEvent(e, localTrack, !localTrack.ExistingFile, downloadClientItem));
|
||||
|
||||
importResults.Add(new ImportResult(importDecision, "Failed to import track, Root folder missing."));
|
||||
}
|
||||
|
@ -269,7 +264,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
album,
|
||||
release,
|
||||
allImportedTrackFiles.Where(s => s.AlbumId == album.Id).ToList(),
|
||||
allOldTrackFiles.Where(s => s.AlbumId == album.Id).ToList(), newDownload,
|
||||
allOldTrackFiles.Where(s => s.AlbumId == album.Id).ToList(), replaceExisting,
|
||||
downloadClientItem));
|
||||
}
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
{
|
||||
public interface IMakeImportDecision
|
||||
{
|
||||
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist);
|
||||
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, bool includeExisting);
|
||||
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo);
|
||||
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease);
|
||||
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease, bool includeExisting);
|
||||
}
|
||||
|
||||
public class ImportDecisionMaker : IMakeImportDecision
|
||||
|
@ -60,22 +60,22 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist)
|
||||
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, bool includeExisting)
|
||||
{
|
||||
return GetImportDecisions(musicFiles, artist, null, null, null, false, false, false);
|
||||
return GetImportDecisions(musicFiles, artist, null, null, null, null, false, false, false, true);
|
||||
}
|
||||
|
||||
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo)
|
||||
{
|
||||
return GetImportDecisions(musicFiles, artist, null, null, folderInfo, false, true, false);
|
||||
return GetImportDecisions(musicFiles, artist, null, null, null, folderInfo, false, true, false, false);
|
||||
}
|
||||
|
||||
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease)
|
||||
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease, bool includeExisting)
|
||||
{
|
||||
var watch = new System.Diagnostics.Stopwatch();
|
||||
watch.Start();
|
||||
|
||||
var files = filterExistingFiles && (artist != null) ? _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist) : musicFiles.ToList();
|
||||
|
||||
var files = filterExistingFiles && (artist != null) ? _mediaFileService.FilterExistingFiles(musicFiles, artist) : musicFiles;
|
||||
|
||||
_logger.Debug("Analyzing {0}/{1} files.", files.Count, musicFiles.Count);
|
||||
|
||||
|
@ -98,7 +98,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
DownloadClientAlbumInfo = downloadClientItemInfo,
|
||||
FolderTrackInfo = folderInfo,
|
||||
Path = file,
|
||||
FileTrackInfo = _audioTagService.ReadTags(file)
|
||||
FileTrackInfo = _audioTagService.ReadTags(file),
|
||||
ExistingFile = !newDownload,
|
||||
AdditionalFile = false
|
||||
};
|
||||
|
||||
try
|
||||
|
@ -121,7 +123,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
|
||||
_logger.Debug($"Tags parsed for {files.Count} files in {watch.ElapsedMilliseconds}ms");
|
||||
|
||||
var releases = _identificationService.Identify(localTracks, artist, album, null, newDownload, singleRelease);
|
||||
var releases = _identificationService.Identify(localTracks, artist, album, albumRelease, newDownload, singleRelease, includeExisting);
|
||||
|
||||
foreach (var release in releases)
|
||||
{
|
||||
|
@ -133,7 +135,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
|||
if (releaseDecision.Approved)
|
||||
{
|
||||
decisions.AddIfNotNull(GetDecision(localTrack));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -11,5 +11,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
public override bool RequiresDiskAccess => true;
|
||||
|
||||
public ImportMode ImportMode { get; set; }
|
||||
public bool ReplaceExistingFiles { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
public QualityModel Quality { get; set; }
|
||||
public Language Language { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public bool DisableReleaseSwitching { get; set; }
|
||||
|
||||
public bool Equals(ManualImportFile other)
|
||||
{
|
||||
|
|
|
@ -24,5 +24,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
public string DownloadId { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
public ParsedTrackInfo Tags { get; set; }
|
||||
public bool AdditionalFile { get; set; }
|
||||
public bool ReplaceExistingFiles { get; set; }
|
||||
public bool DisableReleaseSwitching { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,15 +14,14 @@ using NzbDrone.Core.Parser;
|
|||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Common.Crypto;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||
{
|
||||
public interface IManualImportService
|
||||
{
|
||||
List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles);
|
||||
void UpdateItems(List<ManualImportItem> item);
|
||||
ManualImportItem Find(int id);
|
||||
List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles, bool replaceExistingFiles);
|
||||
List<ManualImportItem> UpdateItems(List<ManualImportItem> item);
|
||||
}
|
||||
|
||||
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
|
||||
|
@ -35,10 +34,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
private readonly IAlbumService _albumService;
|
||||
private readonly IReleaseService _releaseService;
|
||||
private readonly ITrackService _trackService;
|
||||
private readonly IAudioTagService _audioTagService;
|
||||
private readonly IImportApprovedTracks _importApprovedTracks;
|
||||
private readonly ITrackedDownloadService _trackedDownloadService;
|
||||
private readonly IDownloadedTracksImportService _downloadedTracksImportService;
|
||||
private readonly ICached<ManualImportItem> _cache;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
|
@ -50,10 +49,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
IAlbumService albumService,
|
||||
IReleaseService releaseService,
|
||||
ITrackService trackService,
|
||||
IAudioTagService audioTagService,
|
||||
IImportApprovedTracks importApprovedTracks,
|
||||
ITrackedDownloadService trackedDownloadService,
|
||||
IDownloadedTracksImportService downloadedTracksImportService,
|
||||
ICacheManager cacheManager,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
|
@ -65,23 +64,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
_albumService = albumService;
|
||||
_releaseService = releaseService;
|
||||
_trackService = trackService;
|
||||
_audioTagService = audioTagService;
|
||||
_importApprovedTracks = importApprovedTracks;
|
||||
_trackedDownloadService = trackedDownloadService;
|
||||
_downloadedTracksImportService = downloadedTracksImportService;
|
||||
_cache = cacheManager.GetCache<ManualImportItem>(GetType());
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ManualImportItem Find(int id)
|
||||
public List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles, bool replaceExistingFiles)
|
||||
{
|
||||
return _cache.Find(id.ToString());
|
||||
}
|
||||
|
||||
public List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles)
|
||||
{
|
||||
_cache.Clear();
|
||||
|
||||
if (downloadId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var trackedDownload = _trackedDownloadService.Find(downloadId);
|
||||
|
@ -101,23 +93,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
return new List<ManualImportItem>();
|
||||
}
|
||||
|
||||
var decision = _importDecisionMaker.GetImportDecisions(new List<string> { path }, null, null, null, null, false, true, false);
|
||||
var result = MapItem(decision.First(), Path.GetDirectoryName(path), downloadId);
|
||||
_cache.Set(result.Id.ToString(), result);
|
||||
var decision = _importDecisionMaker.GetImportDecisions(new List<string> { path }, null, null, null, null, null, false, true, false, !replaceExistingFiles);
|
||||
var result = MapItem(decision.First(), Path.GetDirectoryName(path), downloadId, replaceExistingFiles, false);
|
||||
|
||||
return new List<ManualImportItem> { result };
|
||||
}
|
||||
|
||||
var items = ProcessFolder(path, downloadId, filterExistingFiles);
|
||||
foreach (var item in items)
|
||||
{
|
||||
_cache.Set(item.Id.ToString(), item);
|
||||
}
|
||||
|
||||
return items;
|
||||
return ProcessFolder(path, downloadId, filterExistingFiles, replaceExistingFiles);
|
||||
}
|
||||
|
||||
private List<ManualImportItem> ProcessFolder(string folder, string downloadId, bool filterExistingFiles)
|
||||
private List<ManualImportItem> ProcessFolder(string folder, string downloadId, bool filterExistingFiles, bool replaceExistingFiles)
|
||||
{
|
||||
var directoryInfo = new DirectoryInfo(folder);
|
||||
var artist = _parsingService.GetArtist(directoryInfo.Name);
|
||||
|
@ -130,24 +115,48 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
|
||||
var folderInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name);
|
||||
var artistFiles = _diskScanService.GetAudioFiles(folder).ToList();
|
||||
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, null, null, folderInfo, filterExistingFiles, true, false);
|
||||
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, null, null, null, folderInfo, filterExistingFiles, true, false, !replaceExistingFiles);
|
||||
|
||||
return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList();
|
||||
// paths will be different for new and old files which is why we need to map separately
|
||||
var newFiles = artistFiles.Join(decisions,
|
||||
f => f,
|
||||
d => d.Item.Path,
|
||||
(f, d) => new { File = f, Decision = d },
|
||||
PathEqualityComparer.Instance);
|
||||
|
||||
var newItems = newFiles.Select(x => MapItem(x.Decision, folder, downloadId, replaceExistingFiles, false));
|
||||
var existingDecisions = decisions.Except(newFiles.Select(x => x.Decision));
|
||||
var existingItems = existingDecisions.Select(x => MapItem(x, x.Item.Artist.Path, null, replaceExistingFiles, false));
|
||||
|
||||
return newItems.Concat(existingItems).ToList();
|
||||
}
|
||||
|
||||
public void UpdateItems(List<ManualImportItem> items)
|
||||
public List<ManualImportItem> UpdateItems(List<ManualImportItem> items)
|
||||
{
|
||||
var groupedItems = items.GroupBy(x => x.Album?.Id);
|
||||
_logger.Debug("UpdateItems, {0} groups", groupedItems.Count());
|
||||
var replaceExistingFiles = items.All(x => x.ReplaceExistingFiles);
|
||||
var groupedItems = items.Where(x => !x.AdditionalFile).GroupBy(x => x.Album?.Id);
|
||||
_logger.Debug($"UpdateItems, {groupedItems.Count()} groups, replaceExisting {replaceExistingFiles}");
|
||||
|
||||
var result = new List<ManualImportItem>();
|
||||
|
||||
foreach(var group in groupedItems)
|
||||
{
|
||||
// generate dummy decisions that don't match the release
|
||||
_logger.Debug("UpdateItems, group key: {0}", group.Key);
|
||||
var decisions = _importDecisionMaker.GetImportDecisions(group.Select(x => x.Path).ToList(), group.First().Artist, group.First().Album, null, null, false, true, true);
|
||||
|
||||
foreach (var decision in decisions)
|
||||
var disableReleaseSwitching = group.First().DisableReleaseSwitching;
|
||||
|
||||
var decisions = _importDecisionMaker.GetImportDecisions(group.Select(x => x.Path).ToList(), group.First().Artist, group.First().Album, group.First().Release, null, null, false, true, true, !replaceExistingFiles);
|
||||
|
||||
var existingItems = group.Join(decisions,
|
||||
i => i.Path,
|
||||
d => d.Item.Path,
|
||||
(i, d) => new { Item = i, Decision = d },
|
||||
PathEqualityComparer.Instance);
|
||||
|
||||
foreach (var pair in existingItems)
|
||||
{
|
||||
var item = items.Where(x => x.Path == decision.Item.Path).Single();
|
||||
var item = pair.Item;
|
||||
var decision = pair.Decision;
|
||||
|
||||
if (decision.Item.Artist != null)
|
||||
{
|
||||
|
@ -167,12 +176,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
|
||||
item.Rejections = decision.Rejections;
|
||||
|
||||
_cache.Set(item.Id.ToString(), item);
|
||||
result.Add(item);
|
||||
}
|
||||
|
||||
var newDecisions = decisions.Except(existingItems.Select(x => x.Decision));
|
||||
result.AddRange(newDecisions.Select(x => MapItem(x, x.Item.Artist.Path, null, replaceExistingFiles, disableReleaseSwitching)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ManualImportItem MapItem(ImportDecision<LocalTrack> decision, string folder, string downloadId)
|
||||
private ManualImportItem MapItem(ImportDecision<LocalTrack> decision, string folder, string downloadId, bool replaceExistingFiles, bool disableReleaseSwitching)
|
||||
{
|
||||
var item = new ManualImportItem();
|
||||
|
||||
|
@ -203,6 +217,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
item.Size = _diskProvider.GetFileSize(decision.Item.Path);
|
||||
item.Rejections = decision.Rejections;
|
||||
item.Tags = decision.Item.FileTrackInfo;
|
||||
item.AdditionalFile = decision.Item.AdditionalFile;
|
||||
item.ReplaceExistingFiles = replaceExistingFiles;
|
||||
item.DisableReleaseSwitching = disableReleaseSwitching;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
@ -220,6 +237,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
{
|
||||
var albumImportDecisions = new List<ImportDecision<LocalTrack>>();
|
||||
|
||||
// turn off anyReleaseOk if specified
|
||||
if (importAlbumId.First().DisableReleaseSwitching)
|
||||
{
|
||||
var album = _albumService.GetAlbum(importAlbumId.First().AlbumId);
|
||||
album.AnyReleaseOk = false;
|
||||
_albumService.UpdateAlbum(album);
|
||||
}
|
||||
|
||||
foreach (var file in importAlbumId)
|
||||
{
|
||||
_logger.ProgressTrace("Processing file {0} of {1}", fileCount + 1, message.Files.Count);
|
||||
|
@ -228,37 +253,35 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
|||
var album = _albumService.GetAlbum(file.AlbumId);
|
||||
var release = _releaseService.GetRelease(file.AlbumReleaseId);
|
||||
var tracks = _trackService.GetTracks(file.TrackIds);
|
||||
var fileTrackInfo = Parser.Parser.ParseMusicPath(file.Path) ?? new ParsedTrackInfo();
|
||||
var fileTrackInfo = _audioTagService.ReadTags(file.Path) ?? new ParsedTrackInfo();
|
||||
|
||||
var localTrack = new LocalTrack
|
||||
{
|
||||
ExistingFile = false,
|
||||
ExistingFile = artist.Path.IsParentPath(file.Path),
|
||||
Tracks = tracks,
|
||||
MediaInfo = null,
|
||||
FileTrackInfo = fileTrackInfo,
|
||||
MediaInfo = fileTrackInfo.MediaInfo,
|
||||
Path = file.Path,
|
||||
Quality = file.Quality,
|
||||
Language = file.Language,
|
||||
Artist = artist,
|
||||
Album = album,
|
||||
Release = release,
|
||||
Size = 0
|
||||
Release = release
|
||||
};
|
||||
|
||||
albumImportDecisions.Add(new ImportDecision<LocalTrack>(localTrack));
|
||||
fileCount += 1;
|
||||
}
|
||||
|
||||
var existingFile = albumImportDecisions.First().Item.Artist.Path.IsParentPath(importAlbumId.First().Path);
|
||||
|
||||
if (importAlbumId.First().DownloadId.IsNullOrWhiteSpace())
|
||||
var downloadId = importAlbumId.Select(x => x.DownloadId).FirstOrDefault(x => x.IsNotNullOrWhiteSpace());
|
||||
if (downloadId.IsNullOrWhiteSpace())
|
||||
{
|
||||
imported.AddRange(_importApprovedTracks.Import(albumImportDecisions, !existingFile, null, message.ImportMode));
|
||||
imported.AddRange(_importApprovedTracks.Import(albumImportDecisions, message.ReplaceExistingFiles, null, message.ImportMode));
|
||||
}
|
||||
else
|
||||
{
|
||||
var trackedDownload = _trackedDownloadService.Find(importAlbumId.First().DownloadId);
|
||||
var importResults = _importApprovedTracks.Import(albumImportDecisions, true, trackedDownload.DownloadItem, message.ImportMode);
|
||||
var trackedDownload = _trackedDownloadService.Find(downloadId);
|
||||
var importResults = _importApprovedTracks.Import(albumImportDecisions, message.ReplaceExistingFiles, trackedDownload.DownloadItem, message.ImportMode);
|
||||
|
||||
imported.AddRange(importResults);
|
||||
|
||||
|
|
|
@ -17,10 +17,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
|
|||
public Decision IsSatisfiedBy(LocalAlbumRelease localAlbumRelease)
|
||||
{
|
||||
var existingRelease = localAlbumRelease.AlbumRelease.Album.Value.AlbumReleases.Value.Single(x => x.Monitored);
|
||||
var existingTrackCount = existingRelease.Tracks.Value.Count(x => x.HasFile);
|
||||
if (localAlbumRelease.AlbumRelease.Id != existingRelease.Id &&
|
||||
localAlbumRelease.TrackCount < existingRelease.Tracks.Value.Count(x => x.HasFile))
|
||||
localAlbumRelease.TrackCount < existingTrackCount)
|
||||
{
|
||||
_logger.Debug("This release has fewer tracks than the existing one. Skipping {0}", localAlbumRelease);
|
||||
_logger.Debug($"This release has fewer tracks ({localAlbumRelease.TrackCount}) than existing {existingRelease} ({existingTrackCount}). Skipping {localAlbumRelease}");
|
||||
return Decision.Reject("Has fewer tracks than existing release");
|
||||
}
|
||||
|
||||
|
|
|
@ -789,6 +789,7 @@
|
|||
<Compile Include="MediaFiles\TrackImport\Specifications\NoMissingOrUnmatchedTracksSpecification.cs" />
|
||||
<Compile Include="MediaFiles\TrackImport\Specifications\ReleaseWantedSpecification.cs" />
|
||||
<Compile Include="MediaFiles\TrackImport\Identification\TrackGroupingService.cs" />
|
||||
<Compile Include="MediaFiles\TrackImport\Identification\CandidateAlbumRelease.cs" />
|
||||
<Compile Include="MediaFiles\TrackImport\Identification\IdentificationService.cs" />
|
||||
<Compile Include="MediaFiles\TrackImport\Identification\IdentificationTestCase.cs" />
|
||||
<Compile Include="MediaFiles\TrackImport\Identification\Distance.cs" />
|
||||
|
|
|
@ -4,6 +4,8 @@ using System.Linq;
|
|||
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
|
||||
using System.IO;
|
||||
using System;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common;
|
||||
|
||||
namespace NzbDrone.Core.Parser.Model
|
||||
{
|
||||
|
@ -33,23 +35,25 @@ namespace NzbDrone.Core.Parser.Model
|
|||
public TrackMapping TrackMapping { get; set; }
|
||||
public Distance Distance { get; set; }
|
||||
public AlbumRelease AlbumRelease { get; set; }
|
||||
public List<LocalTrack> ExistingTracks { get; set; }
|
||||
public bool NewDownload { get; set; }
|
||||
|
||||
public void PopulateMatch()
|
||||
{
|
||||
if (AlbumRelease != null)
|
||||
{
|
||||
LocalTracks = LocalTracks.Concat(ExistingTracks).DistinctBy(x => x.Path).ToList();
|
||||
foreach (var localTrack in LocalTracks)
|
||||
{
|
||||
localTrack.Release = AlbumRelease;
|
||||
localTrack.Album = AlbumRelease.Album.Value;
|
||||
|
||||
localTrack.Artist = localTrack.Album.Artist.Value;
|
||||
|
||||
if (TrackMapping.Mapping.ContainsKey(localTrack))
|
||||
{
|
||||
var track = TrackMapping.Mapping[localTrack].Item1;
|
||||
localTrack.Tracks = new List<Track> { track };
|
||||
localTrack.Distance = TrackMapping.Mapping[localTrack].Item2;
|
||||
localTrack.Artist = localTrack.Album.Artist.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ namespace NzbDrone.Core.Parser.Model
|
|||
public Language Language { get; set; }
|
||||
public MediaInfoModel MediaInfo { get; set; }
|
||||
public bool ExistingFile { get; set; }
|
||||
public bool AdditionalFile { get; set; }
|
||||
public bool SceneSource { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
|
||||
|
|
Loading…
Reference in New Issue