Whole album matching and fingerprinting (#592)

* Cache result of GetAllArtists

* Fixed: Manual import not respecting album import notifications

* Fixed: partial album imports stay in queue, prompting manual import

* Fixed: Allow release if tracks are missing

* Fixed: Be tolerant of missing/extra "The" at start of artist name

* Improve manual import UI

* Omit video tracks from DB entirely

* Revert "faster test packaging in build.sh"

This reverts commit 2723e2a7b8.

-u and -T are not supported on macOS

* Fix tests on linux and macOS

* Actually lint on linux

On linux yarn runs scripts with sh not bash so ** doesn't recursively glob

* Match whole albums

* Option to disable fingerprinting

* Rip out MediaInfo

* Don't split up things that have the same album selected in manual import

* Try to speed up IndentificationService

* More speedups

* Some fixes and increase power of recording id

* Fix NRE when no tags

* Fix NRE when some (but not all) files in a directory have missing tags

* Bump taglib, tidy up tag parsing

* Add a health check

* Remove media info setting

* Tags -> audioTags

* Add some tests where tags are null

* Rename history events

* Add missing method to interface

* Reinstate MediaInfo tags and update info with artist scan

Also adds migration to remove old format media info

* This file no longer exists

* Don't penalise year if missing from tags

* Formatting improvements

* Use correct system newline

* Switch to the netstandard2.0 library to support net 461

* TagLib.File is IDisposable so should be in a using

* Improve filename matching and add tests

* Neater logging of parsed tags

* Fix disk scan tests for new media info update

* Fix quality detection source

* Fix Inexact Artist/Album match

* Add button to clear track mapping

* Fix warning

* Pacify eslint

* Use \ not /

* Fix UI updates

* Fix media covers

Prevent localizing URL propaging back to the metadata object

* Reduce database overhead broadcasting UI updates

* Relax timings a bit to make test pass

* Remove irrelevant tests

* Test framework for identification service

* Fix PreferMissingToBadMatch test case

* Make fingerprinting more robust

* More logging

* Penalize unknown media format and country

* Prefer USA to UK

* Allow Data CD

* Fix exception if fingerprinting fails for all files

* Fix tests

* Fix NRE

* Allow apostrophes and remove accents in filename aggregation

* Address codacy issues

* Cope with old versions of fpcalc and suggest upgrade

* fpcalc health check passes if fingerprinting disabled

* Get the Artist meta with the artist

* Fix the mapper so that lazy loaded lists will be populated on Join

And therefore we can join TrackFiles on Tracks by default and avoid an
extra query

* Rename subtitle -> lyric

* Tidy up MediaInfoFormatter
pull/635/head
ta264 5 years ago committed by Qstick
parent 8bf364945f
commit bb02d73c42

4
.gitignore vendored

@ -45,6 +45,10 @@ _dotCover*
# DevExpress CodeRush
src/.cr/
# Emacs
*~
\#*\#
# NCrunch
*.ncrunch*
.*crunch*.local.xml

@ -2,17 +2,24 @@ language: csharp
os:
- linux
- osx
addons:
apt:
packages:
- dos2unix
- nuget
- libchromaprint-tools
update: true
homebrew:
packages:
- yarn
- dos2unix
- nuget
update: true
solution: src/Lidarr.sln
before_install:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install yarn; fi
- nvm install 8
- nvm use 8
script:
- ./build.sh
- chmod +x test.sh
- ./test.sh Linux Unit
after_success:
- chmod +x package.sh
- ./package.sh
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ./test.sh Mac Unit; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./test.sh Linux Unit; fi

@ -5,7 +5,7 @@ outputFolderLinux='./_output_linux'
outputFolderMacOS='./_output_macos'
outputFolderMacOSApp='./_output_macos_app'
testPackageFolder='./_tests/'
testSearchPattern='*.Test/bin/x86/Release'
testSearchPattern='*.Test/bin/x86/Release/*'
sourceFolder='./src'
slnFile=$sourceFolder/Lidarr.sln
updateFolder=$outputFolder/Lidarr.Update
@ -97,7 +97,11 @@ LintUI()
ProgressEnd 'ESLint'
ProgressStart 'Stylelint'
CheckExitCode yarn stylelint
if [ $runtime = "dotnet" ] ; then
CheckExitCode yarn stylelint-windows
else
CheckExitCode yarn stylelint-linux
fi
ProgressEnd 'Stylelint'
}
@ -171,12 +175,9 @@ PackageMono()
rm -f $outputFolderLinux/ServiceUninstall.*
rm -f $outputFolderLinux/ServiceInstall.*
echo "Removing native windows binaries Sqlite, MediaInfo"
echo "Removing native windows binaries Sqlite, fpcalc"
rm -f $outputFolderLinux/sqlite3.*
rm -f $outputFolderLinux/MediaInfo.*
echo "Adding Lidarr.Core.dll.config (for dllmap)"
cp $sourceFolder/NzbDrone.Core/Lidarr.Core.dll.config $outputFolderLinux
rm -f $outputFolderLinux/fpcalc*
echo "Adding CurlSharp.dll.config (for dllmap)"
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $outputFolderLinux
@ -209,13 +210,11 @@ PackageMacOS()
echo "Copying Binaries"
cp -r $outputFolderLinux/* $outputFolderMacOS
cp $outputFolder/fpcalc $outputFolderMacOS
echo "Adding sqlite dylibs"
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOS
echo "Adding MediaInfo dylib"
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOS
ProgressEnd 'Creating MacOS Package'
}
@ -234,13 +233,11 @@ PackageMacOSApp()
echo "Copying Binaries"
cp -r $outputFolderLinux/* $outputFolderMacOSApp/Lidarr.app/Contents/MacOS
cp $outputFolder/fpcalc $outputFolderMacOSApp/Lidarr.app/Contents/MacOS
echo "Adding sqlite dylibs"
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOSApp/Lidarr.app/Contents/MacOS
echo "Adding MediaInfo dylib"
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOSApp/Lidarr.app/Contents/MacOS
echo "Removing Update Folder"
rm -r $outputFolderMacOSApp/Lidarr.app/Contents/MacOS/Lidarr.Update
@ -254,15 +251,17 @@ PackageTests()
rm -rf $testPackageFolder
mkdir $testPackageFolder
find $sourceFolder -path $testSearchPattern -exec cp -r -u -T "{}" $testPackageFolder \;
find . -maxdepth 6 -path $testSearchPattern -exec cp -r "{}" $testPackageFolder \;
if [ $runtime = "dotnet" ] ; then
$nuget install NUnit.ConsoleRunner -Version 3.7.0 -Output $testPackageFolder
else
mono $nuget install NUnit.ConsoleRunner -Version 3.7.0 -Output $testPackageFolder
nuget install NUnit.ConsoleRunner -Version 3.7.0 -Output $testPackageFolder
fi
cp $outputFolder/*.dll $testPackageFolder
cp $outputFolder/*.exe $testPackageFolder
cp $outputFolder/fpcalc $testPackageFolder
cp ./*.sh $testPackageFolder
echo "Creating MDBs for tests"
@ -281,6 +280,9 @@ PackageTests()
echo "Copying CurlSharp libraries"
cp $sourceFolder/ExternalModules/CurlSharp/libs/i386/* $testPackageFolder
echo "Copying dylibs"
cp -r $outputFolderMacOS/*.dylib $testPackageFolder
ProgressEnd 'Creating Test Package'
}
@ -294,6 +296,9 @@ CleanupWindowsPackage()
echo "Adding Lidarr.Windows to UpdatePackage"
cp $outputFolder/Lidarr.Windows.* $updateFolder
echo "Removing MacOS fpcalc"
rm $outputFolder/fpcalc
ProgressEnd 'Cleaning Windows Package'
}

@ -8,6 +8,33 @@ import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
function getDetailedList(statusMessages) {
return (
<div>
{
statusMessages.map(({ title, messages }) => {
return (
<div key={title}>
{title}
<ul>
{
messages.map((message) => {
return (
<li key={message}>
{message}
</li>
);
})
}
</ul>
</div>
);
})
}
</div>
);
}
function HistoryDetails(props) {
const {
eventType,
@ -124,7 +151,7 @@ function HistoryDetails(props) {
);
}
if (eventType === 'downloadFolderImported') {
if (eventType === 'trackFileImported') {
const {
droppedPath,
importedPath
@ -224,6 +251,113 @@ function HistoryDetails(props) {
</DescriptionList>
);
}
if (eventType === 'albumImportIncomplete') {
const {
statusMessages
} = data;
return (
<DescriptionList>
<DescriptionListItem
title="Name"
data={sourceTitle}
/>
{
!!statusMessages &&
<DescriptionListItem
title="Import failures"
data={getDetailedList(JSON.parse(statusMessages))}
/>
}
</DescriptionList>
);
}
if (eventType === 'downloadImported') {
const {
indexer,
releaseGroup,
nzbInfoUrl,
downloadClient,
downloadId,
age,
ageHours,
ageMinutes,
publishedDate
} = data;
return (
<DescriptionList>
<DescriptionListItem
title="Name"
data={sourceTitle}
/>
{
!!indexer &&
<DescriptionListItem
title="Indexer"
data={indexer}
/>
}
{
!!releaseGroup &&
<DescriptionListItem
title="Release Group"
data={releaseGroup}
/>
}
{
!!nzbInfoUrl &&
<span>
<DescriptionListItemTitle>
Info URL
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span>
}
{
!!downloadClient &&
<DescriptionListItem
title="Download Client"
data={downloadClient}
/>
}
{
!!downloadId &&
<DescriptionListItem
title="Grab ID"
data={downloadId}
/>
}
{
!!indexer &&
<DescriptionListItem
title="Age (when grabbed)"
data={formatAge(age, ageHours, ageMinutes)}
/>
}
{
!!publishedDate &&
<DescriptionListItem
title="Published Date"
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/>
}
</DescriptionList>
);
}
}
HistoryDetails.propTypes = {

@ -17,12 +17,16 @@ function getHeaderTitle(eventType) {
return 'Grabbed';
case 'downloadFailed':
return 'Download Failed';
case 'downloadFolderImported':
case 'trackFileImported':
return 'Track Imported';
case 'trackFileDeleted':
return 'Track File Deleted';
case 'trackFileRenamed':
return 'Track File Renamed';
case 'albumImportIncomplete':
return 'Album Import Incomplete';
case 'downloadImported':
return 'Download Completed';
default:
return 'Unknown';
}

@ -11,7 +11,7 @@ function getIconName(eventType) {
return icons.DOWNLOADING;
case 'artistFolderImported':
return icons.DRIVE;
case 'downloadFolderImported':
case 'trackFileImported':
return icons.DOWNLOADED;
case 'downloadFailed':
return icons.DOWNLOADING;
@ -19,6 +19,10 @@ function getIconName(eventType) {
return icons.DELETE;
case 'trackFileRenamed':
return icons.ORGANIZE;
case 'albumImportIncomplete':
return icons.DOWNLOADED;
case 'downloadImported':
return icons.DOWNLOADED;
default:
return icons.UNKNOWN;
}
@ -28,6 +32,8 @@ function getIconKind(eventType) {
switch (eventType) {
case 'downloadFailed':
return kinds.DANGER;
case 'albumImportIncomplete':
return kinds.WARNING;
default:
return kinds.DEFAULT;
}
@ -39,7 +45,7 @@ function getTooltip(eventType, data) {
return `Album grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
case 'artistFolderImported':
return 'Track imported from artist folder';
case 'downloadFolderImported':
case 'trackFileImported':
return 'Track downloaded successfully and picked up from download client';
case 'downloadFailed':
return 'Album download failed';
@ -47,6 +53,10 @@ function getTooltip(eventType, data) {
return 'Track file deleted';
case 'trackFileRenamed':
return 'Track file renamed';
case 'albumImportIncomplete':
return 'Files downloaded but not all could be imported';
case 'downloadImported':
return 'Download completed and successfully imported';
default:
return 'Unknown event';
}

@ -18,7 +18,7 @@ function getTitle(eventType) {
switch (eventType) {
case 'grabbed': return 'Grabbed';
case 'artistFolderImported': return 'Artist Folder Imported';
case 'downloadFolderImported': return 'Download Folder Imported';
case 'trackFileImported': return 'Download Folder Imported';
case 'downloadFailed': return 'Download Failed';
case 'trackFileDeleted': return 'Track File Deleted';
case 'trackFileRenamed': return 'Track File Renamed';

@ -53,6 +53,7 @@ import {
faEye as fasEye,
faFastBackward as fasFastBackward,
faFastForward as fasFastForward,
faFileImport as fasFileImport,
faFilter as fasFilter,
faFolderOpen as fasFolderOpen,
faForward as fasForward,
@ -137,6 +138,7 @@ export const EXPAND_INDETERMINATE = fasChevronCircleRight;
export const EXTERNAL_LINK = fasExternalLinkAlt;
export const FATAL = fasTimesCircle;
export const FILE = farFile;
export const FILEIMPORT = fasFileImport;
export const FILTER = fasFilter;
export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen;

@ -59,16 +59,19 @@ class SelectAlbumModalContentConnector extends Component {
onAlbumSelect = (albumId) => {
const album = _.find(this.props.items, { id: albumId });
this.props.ids.forEach((id) => {
const ids = this.props.ids;
ids.forEach((id) => {
this.props.updateInteractiveImportItem({
id,
album,
tracks: [],
rejections: []
});
this.props.saveInteractiveImportItem({ id });
});
this.props.saveInteractiveImportItem({ id: ids });
this.props.onModalClose(true);
}

@ -0,0 +1,65 @@
.fileDetails {
margin-bottom: 20px;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
&:last-of-type {
margin-bottom: 0;
}
}
.header {
position: relative;
display: flex;
align-items: center;
width: 100%;
font-size: 18px;
}
.filename {
flex-grow: 1;
margin-right: 10px;
margin-left: 10px;
}
.expandButton {
position: relative;
width: 60px;
height: 60px;
}
.actionButton {
composes: button from 'Components/Link/IconButton.css';
width: 30px;
}
.audioTags {
padding-top: 15px;
padding-bottom: 15px;
border-top: 1px solid $borderColor;
}
.expandButtonIcon {
composes: actionButton;
position: absolute;
top: 50%;
left: 50%;
margin-top: -12px;
margin-left: -15px;
}
@media only screen and (max-width: $breakpointSmall) {
.medium {
border-right: 0;
border-left: 0;
border-radius: 0;
}
.expandButtonIcon {
position: static;
margin: 0;
}
}

@ -0,0 +1,258 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import styles from './FileDetails.css';
class FileDetails extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isExpanded: props.isExpanded
};
}
//
// Listeners
onExpandPress = () => {
const {
isExpanded
} = this.state;
this.setState({ isExpanded: !isExpanded });
}
//
// Render
renderRejections() {
const {
rejections
} = this.props;
return (
<span>
<DescriptionListItemTitle>
Rejections
</DescriptionListItemTitle>
{
_.map(rejections, (item, key) => {
return (
<DescriptionListItemDescription key={key}>
{item.reason}
</DescriptionListItemDescription>
);
})
}
</span>
);
}
render() {
const {
filename,
audioTags,
rejections
} = this.props;
const {
isExpanded
} = this.state;
return (
<div
className={styles.fileDetails}
>
<div className={styles.header} onClick={this.onExpandPress}>
<div className={styles.filename}>
{filename}
</div>
<div className={styles.expandButton}>
<Icon
className={styles.expandButtonIcon}
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
title={isExpanded ? 'Hide file info' : 'Show file info'}
size={24}
/>
</div>
</div>
<div>
{
isExpanded &&
<div className={styles.audioTags}>
<DescriptionList>
{
audioTags.title !== undefined &&
<DescriptionListItem
title="Track Title"
data={audioTags.title}
/>
}
{
audioTags.trackNumbers[0] > 0 &&
<DescriptionListItem
title="Track Number"
data={audioTags.trackNumbers[0]}
/>
}
{
audioTags.discNumber > 0 &&
<DescriptionListItem
title="Disc Number"
data={audioTags.discNumber}
/>
}
{
audioTags.discCount > 0 &&
<DescriptionListItem
title="Disc Count"
data={audioTags.discCount}
/>
}
{
audioTags.albumTitle !== undefined &&
<DescriptionListItem
title="Album"
data={audioTags.albumTitle}
/>
}
{
audioTags.artistTitle !== undefined &&
<DescriptionListItem
title="Artist"
data={audioTags.artistTitle}
/>
}
{
audioTags.country !== undefined &&
<DescriptionListItem
title="Country"
data={audioTags.country.name}
/>
}
{
audioTags.year > 0 &&
<DescriptionListItem
title="Year"
data={audioTags.year}
/>
}
{
audioTags.label !== undefined &&
<DescriptionListItem
title="Label"
data={audioTags.label}
/>
}
{
audioTags.catalogNumber !== undefined &&
<DescriptionListItem
title="Catalog Number"
data={audioTags.catalogNumber}
/>
}
{
audioTags.disambiguation !== undefined &&
<DescriptionListItem
title="Disambiguation"
data={audioTags.disambiguation}
/>
}
{
audioTags.duration !== undefined &&
<DescriptionListItem
title="Duration"
data={formatTimeSpan(audioTags.duration)}
/>
}
{
audioTags.artistMBId !== undefined &&
<Link
to={`https://musicbrainz.org/artist/${audioTags.artistMBId}`}
>
<DescriptionListItem
title="MusicBrainz Artist ID"
data={audioTags.artistMBId}
/>
</Link>
}
{
audioTags.albumMBId !== undefined &&
<Link
to={`https://musicbrainz.org/release-group/${audioTags.albumMBId}`}
>
<DescriptionListItem
title="MusicBrainz Album ID"
data={audioTags.albumMBId}
/>
</Link>
}
{
audioTags.releaseMBId !== undefined &&
<Link
to={`https://musicbrainz.org/release/${audioTags.releaseMBId}`}
>
<DescriptionListItem
title="MusicBrainz Release ID"
data={audioTags.releaseMBId}
/>
</Link>
}
{
audioTags.recordingMBId !== undefined &&
<Link
to={`https://musicbrainz.org/recording/${audioTags.recordingMBId}`}
>
<DescriptionListItem
title="MusicBrainz Recording ID"
data={audioTags.recordingMBId}
/>
</Link>
}
{
audioTags.trackMBId !== undefined &&
<Link
to={`https://musicbrainz.org/track/${audioTags.trackMBId}`}
>
<DescriptionListItem
title="MusicBrainz Track ID"
data={audioTags.trackMBId}
/>
</Link>
}
{
rejections.length > 0 &&
this.renderRejections()
}
</DescriptionList>
</div>
}
</div>
</div>
);
}
}
FileDetails.propTypes = {
audioTags: PropTypes.object.isRequired,
filename: PropTypes.string.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
isExpanded: PropTypes.bool
};
export default FileDetails;

@ -19,12 +19,13 @@
.centerButtons,
.rightButtons {
display: flex;
flex: 1 0 33%;
flex: 1 2 25%;
flex-wrap: wrap;
}
.centerButtons {
justify-content: center;
flex: 2 1 50%;
}
.rightButtons {

@ -155,6 +155,18 @@ class InteractiveImportModalContent extends Component {
this.setState({ isSelectAlbumModalOpen: true });
}
onClearTrackMappingPress = () => {
const selectedIds = this.getSelectedIds();
selectedIds.forEach((id) => {
this.props.updateInteractiveImportItem({
id,
tracks: [],
rejections: []
});
});
}
onSelectArtistModalClose = () => {
this.setState({ isSelectArtistModalOpen: false });
}
@ -328,6 +340,10 @@ class InteractiveImportModalContent extends Component {
>
Select Album
</Button>
<Button onPress={this.onClearTrackMappingPress}>
Clear Track Mapping
</Button>
</div>
<div className={styles.rightButtons}>
@ -362,6 +378,7 @@ class InteractiveImportModalContent extends Component {
artistId={selectedItem && selectedItem.artist && selectedItem.artist.id}
onModalClose={this.onSelectAlbumModalClose}
/>
</ModalContent>
);
}
@ -387,6 +404,7 @@ InteractiveImportModalContent.propTypes = {
onFilterExistingFilesChange: PropTypes.func.isRequired,
onImportModeChange: PropTypes.func.isRequired,
onImportSelectedPress: PropTypes.func.isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
@ -23,6 +23,7 @@ const mapDispatchToProps = {
setInteractiveImportSort,
setInteractiveImportMode,
clearInteractiveImport,
updateInteractiveImportItem,
executeCommand
};
@ -195,6 +196,7 @@ InteractiveImportModalContentConnector.propTypes = {
setInteractiveImportSort: PropTypes.func.isRequired,
clearInteractiveImport: PropTypes.func.isRequired,
setInteractiveImportMode: PropTypes.func.isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import formatBytes from 'Utilities/Number/formatBytes';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { icons, kinds, tooltipPositions, sortDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
@ -167,11 +167,13 @@ class InteractiveImportRow extends Component {
relativePath,
artist,
album,
albumReleaseId,
tracks,
quality,
language,
size,
rejections,
audioTags,
isSelected,
onSelectedChange
} = this.props;
@ -327,6 +329,11 @@ class InteractiveImportRow extends Component {
id={id}
artistId={artist && artist.id}
albumId={album && album.id}
albumReleaseId={albumReleaseId}
rejections={rejections}
audioTags={audioTags}
sortKey='mediumNumber'
sortDirection={sortDirections.ASCENDING}
filename={relativePath}
onModalClose={this.onSelectTrackModalClose}
/>
@ -358,11 +365,13 @@ InteractiveImportRow.propTypes = {
relativePath: PropTypes.string.isRequired,
artist: PropTypes.object,
album: PropTypes.object,
albumReleaseId: PropTypes.number,
tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object,
language: PropTypes.object,
size: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
audioTags: PropTypes.object.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
onValidRowChange: PropTypes.func.isRequired

@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import _ from 'lodash';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
@ -14,6 +15,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import SelectTrackRow from './SelectTrackRow';
import FileDetails from '../FileDetails';
const columns = [
{
@ -32,6 +34,19 @@ const columns = [
name: 'title',
label: 'Title',
isVisible: true
},
{
name: 'trackStatus',
label: 'Status',
isVisible: true
}
];
const selectAllBlankColumn = [
{
name: 'dummy',
label: ' ',
isVisible: true
}
];
@ -43,12 +58,17 @@ class SelectTrackModalContent extends Component {
constructor(props, context) {
super(props, context);
const selectedTracks = _.filter(props.selectedTracksByItem, ['id', props.id])[0].tracks;
const init = _.zipObject(selectedTracks, _.times(selectedTracks.length, _.constant(true)));
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
selectedState: init
};
props.onSortPress( props.sortKey, props.sortDirection );
}
//
@ -80,6 +100,9 @@ class SelectTrackModalContent extends Component {
render() {
const {
id,
audioTags,
rejections,
isFetching,
isPopulated,
error,
@ -88,6 +111,7 @@ class SelectTrackModalContent extends Component {
sortDirection,
onSortPress,
onModalClose,
selectedTracksByItem,
filename
} = this.props;
@ -97,13 +121,23 @@ class SelectTrackModalContent extends Component {
selectedState
} = this.state;
const title = `Manual Import - Select Track(s): ${filename}`;
const errorMessage = getErrorMessage(error, 'Unable to load tracks');
// all tracks selected for other items
const otherSelected = _.map(_.filter(selectedTracksByItem, (item) => {
return item.id !== id;
}), (x) => {
return x.tracks;
}).flat();
// tracks selected for the current file
const currentSelected = _.keys(_.pickBy(selectedState, _.identity)).map(Number);
// only enable selectAll if no other files have any tracks selected.
const selectAllEnabled = otherSelected.length === 0;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{title}
Manual Import - Select Track(s):
</ModalHeader>
<ModalBody>
@ -117,11 +151,18 @@ class SelectTrackModalContent extends Component {
<div>{errorMessage}</div>
}
<FileDetails
audioTags={audioTags}
filename={filename}
rejections={rejections}
isExpanded={false}
/>
{
isPopulated && !!items.length &&
<Table
columns={columns}
selectAll={true}
columns={selectAllEnabled ? columns : selectAllBlankColumn.concat(columns)}
selectAll={selectAllEnabled}
allSelected={allSelected}
allUnselected={allUnselected}
sortKey={sortKey}
@ -139,6 +180,9 @@ class SelectTrackModalContent extends Component {
mediumNumber={item.mediumNumber}
trackNumber={item.absoluteTrackNumber}
title={item.title}
hasFile={item.hasFile}
importSelected={otherSelected.concat(currentSelected).includes(item.id)}
isDisabled={otherSelected.includes(item.id)}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
@ -173,6 +217,9 @@ class SelectTrackModalContent extends Component {
}
SelectTrackModalContent.propTypes = {
id: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
audioTags: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
@ -182,6 +229,7 @@ SelectTrackModalContent.propTypes = {
onSortPress: PropTypes.func.isRequired,
onTracksSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
selectedTracksByItem: PropTypes.arrayOf(PropTypes.object).isRequired,
filename: PropTypes.string.isRequired
};

@ -11,8 +11,19 @@ import SelectTrackModalContent from './SelectTrackModalContent';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('tracks'),
(tracks) => {
return tracks;
createClientSideCollectionSelector('interactiveImport'),
(tracks, interactiveImport) => {
const selectedTracksByItem = _.map(interactiveImport.items, (item) => {
return { id: item.id, tracks: _.map(item.tracks, (track) => {
return track.id;
}) };
});
return {
...tracks,
selectedTracksByItem
};
}
);
}
@ -32,10 +43,11 @@ class SelectTrackModalContentConnector extends Component {
componentDidMount() {
const {
artistId,
albumId
albumId,
albumReleaseId
} = this.props;
this.props.fetchTracks({ artistId, albumId });
this.props.fetchTracks({ artistId, albumId, albumReleaseId });
}
componentWillUnmount() {
@ -86,6 +98,9 @@ SelectTrackModalContentConnector.propTypes = {
id: PropTypes.number.isRequired,
artistId: PropTypes.number.isRequired,
albumId: PropTypes.number.isRequired,
albumReleaseId: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
audioTags: PropTypes.object.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchTracks: PropTypes.func.isRequired,
setTracksSort: PropTypes.func.isRequired,

@ -3,6 +3,9 @@ import React, { Component } from 'react';
import TableRowButton from 'Components/Table/TableRowButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
class SelectTrackRow extends Component {
@ -27,16 +30,50 @@ class SelectTrackRow extends Component {
mediumNumber,
trackNumber,
title,
hasFile,
importSelected,
isSelected,
isDisabled,
onSelectedChange
} = this.props;
let iconName = icons.UNKNOWN;
let iconKind = kinds.DEFAULT;
let iconTip = '';
if (hasFile && !importSelected) {
iconName = icons.DOWNLOADED;
iconKind = kinds.DEFAULT;
iconTip = 'Track already in library.';
} else if (!hasFile && !importSelected) {
iconName = icons.UNKNOWN;
iconKind = kinds.DEFAULT;
iconTip = 'Track missing from library and no import selected.';
} else if (importSelected && hasFile) {
iconName = icons.FILEIMPORT;
iconKind = kinds.WARNING;
iconTip = 'Warning: Existing track will be replaced by download.';
} else if (importSelected && !hasFile) {
iconName = icons.FILEIMPORT;
iconKind = kinds.DEFAULT;
iconTip = 'Track missing from library and selected for import.';
}
// isDisabled can only be true if importSelected is true
if (isDisabled) {
iconTip = `${iconTip}\nAnother file is selected to import for this track.`;
}
return (
<TableRowButton onPress={this.onPress}>
<TableRowButton
onPress={this.onPress}
isDisabled={isDisabled}
>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
isDisabled={isDisabled}
/>
<TableRowCell>
@ -51,6 +88,19 @@ class SelectTrackRow extends Component {
{title}
</TableRowCell>
<TableRowCell>
<Popover
anchor={
<Icon
name={iconName}
kind={iconKind}
/>
}
title={'Track status'}
body={iconTip}
position={tooltipPositions.LEFT}
/>
</TableRowCell>
</TableRowButton>
);
}
@ -61,7 +111,10 @@ SelectTrackRow.propTypes = {
mediumNumber: PropTypes.number.isRequired,
trackNumber: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
hasFile: PropTypes.bool.isRequired,
importSelected: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
isDisabled: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};

@ -18,6 +18,12 @@ const rescanAfterRefreshOptions = [
{ key: 'never', value: 'Never' }
];
const allowFingerprintingOptions = [
{ key: 'allFiles', value: 'Always' },
{ key: 'newFiles', value: 'For new imports only' },
{ key: 'never', value: 'Never' }
];
const fileDateOptions = [
{ key: 'none', value: 'None' },
{ key: 'albumReleaseDate', value: 'Album Release Date' }
@ -212,16 +218,17 @@ class MediaManagement extends Component {
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>Analyse audio files</FormLabel>
<FormLabel>Rescan Artist Folder after Refresh</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableMediaInfo"
helpText="Extract audio information such as bitrate, runtime and codec information from files. This requires Lidarr to read parts of the file which may cause high disk or network activity during scans."
type={inputTypes.SELECT}
name="rescanAfterRefresh"
helpText="Rescan the artist folder after refreshing the artist"
helpTextWarning="Lidarr will not automatically detect changes to files when not set to 'Always'"
values={rescanAfterRefreshOptions}
onChange={onInputChange}
{...settings.enableMediaInfo}
{...settings.rescanAfterRefresh}
/>
</FormGroup>
@ -229,16 +236,16 @@ class MediaManagement extends Component {
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>Rescan Artist Folder after Refresh</FormLabel>
<FormLabel>Allow Fingerprinting</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="rescanAfterRefresh"
helpText="Rescan the artist folder after refreshing the artist"
helpTextWarning="Lidarr will not automatically detect changes to files when not set to 'Always'"
values={rescanAfterRefreshOptions}
name="allowFingerprinting"
helpText="Use fingerprinting to improve accuracy of track matching"
helpTextWarning="This requires Lidarr to read parts of the file which will slow down scans and may cause high disk or network activity."
values={allowFingerprintingOptions}
onChange={onInputChange}
{...settings.rescanAfterRefresh}
{...settings.allowFingerprinting}
/>
</FormGroup>

@ -168,11 +168,10 @@ class NamingModal extends Component {
];
const mediaInfoTokens = [
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]' },
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
{ token: '{MediaInfo AudioFormat}', example: 'DTS' },
{ token: '{MediaInfo AudioChannels}', example: '5.1' }
{ token: '{MediaInfo AudioCodec}', example: 'FLAC' },
{ token: '{MediaInfo AudioChannels}', example: '2.0' },
{ token: '{MediaInfo AudioBitsPerSample}', example: '24bit' },
{ token: '{MediaInfo AudioSampleRate}', example: '44.1kHz' }
];
const releaseGroupTokens = [

@ -25,7 +25,7 @@ function createSaveProviderHandler(section, url, options = {}) {
...otherPayload
} = payload;
const saveData = getProviderState({ id, ...otherPayload }, getState, section);
const saveData = Array.isArray(id) ? id.map((x) => getProviderState({ id: x, ...otherPayload }, getState, section)) : getProviderState({ id, ...otherPayload }, getState, section);
const ajaxOptions = {
url: `${url}?${$.param(queryParams, true)}`,
@ -36,8 +36,10 @@ function createSaveProviderHandler(section, url, options = {}) {
};
if (id) {
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
ajaxOptions.method = 'PUT';
if (!Array.isArray(id)) {
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
}
}
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
@ -45,16 +47,18 @@ function createSaveProviderHandler(section, url, options = {}) {
abortCurrentRequests[section] = abortRequest;
request.done((data) => {
dispatch(batchActions([
updateItem({ section, ...data }),
set({
section,
isSaving: false,
saveError: null,
pendingChanges: {}
})
]));
if (!Array.isArray(data)) {
data = [data];
}
dispatch(batchActions(
data.map((item) => updateItem({ section, ...item })).concat(
set({
section,
isSaving: false,
saveError: null,
pendingChanges: {}
})
)));
});
request.fail((xhr) => {

@ -76,8 +76,7 @@ export const defaultState = {
name: 'artistType',
label: 'Type',
isSortable: true,
isVisible: true,
isModifiable: false
isVisible: true
},
{
name: 'qualityProfileId',

@ -109,8 +109,8 @@ export const defaultState = {
]
},
{
key: 'imported',
label: 'Imported',
key: 'trackFileImported',
label: 'Track Imported',
filters: [
{
key: 'eventType',
@ -121,7 +121,7 @@ export const defaultState = {
},
{
key: 'failed',
label: 'Failed',
label: 'Download Failed',
filters: [
{
key: 'eventType',
@ -130,6 +130,28 @@ export const defaultState = {
}
]
},
{
key: 'importFailed',
label: 'Import Failed',
filters: [
{
key: 'eventType',
value: '7',
type: filterTypes.EQUAL
}
]
},
{
key: 'downloadImported',
label: 'Download Imported',
filters: [
{
key: 'eventType',
value: '8',
type: filterTypes.EQUAL
}
]
},
{
key: 'deleted',
label: 'Deleted',

@ -7,8 +7,9 @@
"start": "gulp watch",
"clean": "rimraf ./_output/UI",
"eslint": "esprint check",
"eslint-fix": "eslint start --fix",
"stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
"eslint-fix": "eslint --fix frontend/** ",
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
},
"repository": {
"type": "git",

@ -7,12 +7,20 @@ using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Music;
using NzbDrone.SignalR;
using Lidarr.Http.Extensions;
using Lidarr.Http.REST;
using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.Music.Events;
using NzbDrone.Core.MediaFiles.Events;
namespace Lidarr.Api.V1.Albums
{
public class AlbumModule : AlbumModuleWithSignalR
public class AlbumModule : AlbumModuleWithSignalR,
IHandle<AlbumGrabbedEvent>,
IHandle<AlbumEditedEvent>,
IHandle<AlbumImportedEvent>,
IHandle<TrackImportedEvent>
{
protected readonly IReleaseService _releaseService;
@ -83,5 +91,31 @@ namespace Lidarr.Api.V1.Albums
return MapToResource(_albumService.GetAlbums(resource.AlbumIds), false).AsResponse(HttpStatusCode.Accepted);
}
public void Handle(AlbumGrabbedEvent message)
{
foreach (var album in message.Album.Albums)
{
var resource = album.ToResource();
resource.Grabbed = true;
BroadcastResourceChange(ModelAction.Updated, resource);
}
}
public void Handle(AlbumEditedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
}
public void Handle(AlbumImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true));
}
public void Handle(TrackImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.TrackInfo.Album.ToResource());
}
}
}

@ -1,9 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Common.Extensions;
using Lidarr.Api.V1.Artist;
using NzbDrone.Core.DecisionEngine;
@ -11,16 +7,10 @@ using NzbDrone.Core.Music;
using NzbDrone.Core.ArtistStats;
using NzbDrone.SignalR;
using Lidarr.Http;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Music.Events;
namespace Lidarr.Api.V1.Albums
{
public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR<AlbumResource, Album>,
IHandle<AlbumGrabbedEvent>,
IHandle<AlbumEditedEvent>,
IHandle<TrackImportedEvent>
public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR<AlbumResource, Album>
{
protected readonly IAlbumService _albumService;
protected readonly IArtistStatisticsService _artistStatisticsService;
@ -124,34 +114,5 @@ namespace Lidarr.Api.V1.Albums
}
}
public void Handle(AlbumGrabbedEvent message)
{
foreach (var album in message.Album.Albums)
{
var resource = album.ToResource();
resource.Grabbed = true;
BroadcastResourceChange(ModelAction.Updated, resource);
}
}
public void Handle(AlbumEditedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Album.Id);
}
public void Handle(TrackImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.AlbumId);
}
//public void Handle(TrackDownloadedEvent message)
//{
// foreach (var album in message.Album.Albums)
// {
// BroadcastResourceChange(ModelAction.Updated, album.Id);
// }
//}
}
}

@ -24,7 +24,7 @@ using Lidarr.Http.Extensions;
namespace Lidarr.Api.V1.Artist
{
public class ArtistModule : LidarrRestModuleWithSignalR<ArtistResource, NzbDrone.Core.Music.Artist>,
IHandle<TrackImportedEvent>,
IHandle<AlbumImportedEvent>,
IHandle<TrackFileDeletedEvent>,
IHandle<ArtistUpdatedEvent>,
IHandle<ArtistEditedEvent>,
@ -236,26 +236,26 @@ namespace Lidarr.Api.V1.Artist
resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path);
}
public void Handle(TrackImportedEvent message)
public void Handle(AlbumImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.TrackInfo.Artist.ToResource());
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
}
public void Handle(TrackFileDeletedEvent message)
{
if (message.Reason == DeleteMediaFileReason.Upgrade) return;
BroadcastResourceChange(ModelAction.Updated, message.TrackFile.ArtistId);
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.TrackFile.Artist.Value));
}
public void Handle(ArtistUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
}
public void Handle(ArtistEditedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
}
public void Handle(ArtistDeletedEvent message)
@ -270,7 +270,7 @@ namespace Lidarr.Api.V1.Artist
public void Handle(MediaCoversUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Artist.Id);
BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist));
}
}
}

@ -81,7 +81,7 @@ namespace Lidarr.Api.V1.Artist
ArtistType = model.Metadata.Value.Type,
Disambiguation = model.Metadata.Value.Disambiguation,
Images = model.Metadata.Value.Images,
Images = model.Metadata.Value.Images.JsonClone(),
Path = model.Path,
QualityProfileId = model.ProfileId,

@ -13,6 +13,7 @@ namespace Lidarr.Api.V1.Config
public bool DeleteEmptyFolders { get; set; }
public FileDateType FileDate { get; set; }
public RescanAfterRefreshType RescanAfterRefresh { get; set; }
public AllowFingerprinting AllowFingerprinting { get; set; }
public bool SetPermissionsLinux { get; set; }
public string FileChmod { get; set; }
@ -24,7 +25,6 @@ namespace Lidarr.Api.V1.Config
public bool CopyUsingHardlinks { get; set; }
public bool ImportExtraFiles { get; set; }
public string ExtraFileExtensions { get; set; }
public bool EnableMediaInfo { get; set; }
}
public static class MediaManagementConfigResourceMapper
@ -40,6 +40,7 @@ namespace Lidarr.Api.V1.Config
DeleteEmptyFolders = model.DeleteEmptyFolders,
FileDate = model.FileDate,
RescanAfterRefresh = model.RescanAfterRefresh,
AllowFingerprinting = model.AllowFingerprinting,
SetPermissionsLinux = model.SetPermissionsLinux,
FileChmod = model.FileChmod,
@ -51,7 +52,6 @@ namespace Lidarr.Api.V1.Config
CopyUsingHardlinks = model.CopyUsingHardlinks,
ImportExtraFiles = model.ImportExtraFiles,
ExtraFileExtensions = model.ExtraFileExtensions,
EnableMediaInfo = model.EnableMediaInfo
};
}
}

@ -2,12 +2,11 @@ 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 NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Music;
using NLog;
using Nancy;
namespace Lidarr.Api.V1.ManualImport
{
@ -30,7 +29,13 @@ namespace Lidarr.Api.V1.ManualImport
_releaseService = releaseService;
GetResourceAll = GetMediaFiles;
UpdateResource = UpdateImportItem;
Put["/"] = options =>
{
var resource = Request.Body.FromJson<List<ManualImportResource>>();
UpdateImportItems(resource);
return GetManualImportItems(resource.Select(x => x.Id)).AsResponse(HttpStatusCode.Accepted);
};
}
private List<ManualImportResource> GetMediaFiles()
@ -54,26 +59,29 @@ namespace Lidarr.Api.V1.ManualImport
return item;
}
private void UpdateImportItem(ManualImportResource resource)
private void UpdateImportItems(List<ManualImportResource> resources)
{
var item = new ManualImportItem{
Id = resource.Id,
Path = resource.Path,
RelativePath = resource.RelativePath,
FolderName = resource.FolderName,
Name = resource.Name,
Size = resource.Size,
Artist = resource.Artist == null ? null : _artistService.GetArtist(resource.Artist.Id),
Album = resource.Album == null ? null : _albumService.GetAlbum(resource.Album.Id),
Release = resource.AlbumReleaseId == 0 ? null : _releaseService.GetRelease(resource.AlbumReleaseId),
Quality = resource.Quality,
Language = resource.Language,
DownloadId = resource.DownloadId
};
var items = new List<ManualImportItem>();
foreach (var resource in resources)
{
items.Add(new ManualImportItem {
Id = resource.Id,
Path = resource.Path,
RelativePath = resource.RelativePath,
FolderName = resource.FolderName,
Name = resource.Name,
Size = resource.Size,
Artist = resource.Artist == null ? null : _artistService.GetArtist(resource.Artist.Id),
Album = resource.Album == null ? null : _albumService.GetAlbum(resource.Album.Id),
Release = resource.AlbumReleaseId == 0 ? null : _releaseService.GetRelease(resource.AlbumReleaseId),
Quality = resource.Quality,
Language = resource.Language,
DownloadId = resource.DownloadId
});
}
//recalculate import and broadcast
_manualImportService.UpdateItem(item);
BroadcastResourceChange(ModelAction.Updated, item.Id);
_manualImportService.UpdateItems(items);
}
}
}

@ -41,5 +41,10 @@ namespace Lidarr.Api.V1.ManualImport
{
return _manualImportService.Find(id).ToResource();
}
protected List<ManualImportResource> GetManualImportItems(IEnumerable<int> ids)
{
return ids.Select(x => _manualImportService.Find(x).ToResource()).ToList();
}
}
}

@ -10,6 +10,7 @@ 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
{
@ -29,6 +30,7 @@ namespace Lidarr.Api.V1.ManualImport
public int QualityWeight { get; set; }
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }
public ParsedTrackInfo AudioTags { get; set; }
}
public static class ManualImportResourceMapper
@ -53,7 +55,8 @@ namespace Lidarr.Api.V1.ManualImport
Language = model.Language,
//QualityWeight
DownloadId = model.DownloadId,
Rejections = model.Rejections
Rejections = model.Rejections,
AudioTags = model.Tags
};
}

@ -1,5 +1,6 @@
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.MediaFiles;
using Lidarr.Http.REST;
using NzbDrone.Core.Parser.Model;
namespace Lidarr.Api.V1.TrackFiles
{

@ -53,9 +53,8 @@ namespace Lidarr.Api.V1.TrackFiles
private TrackFileResource GetTrackFile(int id)
{
var trackFile = _mediaFileService.Get(id);
var artist = _artistService.GetArtist(trackFile.ArtistId);
return trackFile.ToResource(artist, _upgradableSpecification);
return trackFile.ToResource(trackFile.Artist.Value, _upgradableSpecification);
}
private List<TrackFileResource> GetTrackFiles()
@ -94,12 +93,9 @@ namespace Lidarr.Api.V1.TrackFiles
.Select(e => Convert.ToInt32(e))
.ToList();
// trackfiles will come back with the artist already populated
var trackFiles = _mediaFileService.Get(trackFileIds);
return trackFiles.GroupBy(e => e.ArtistId)
.SelectMany(f => f.ToList()
.ConvertAll(e => e.ToResource(_artistService.GetArtist(f.Key), _upgradableSpecification)))
.ToList();
return trackFiles.ConvertAll(e => e.ToResource(e.Artist.Value, _upgradableSpecification));
}
}
@ -113,7 +109,7 @@ namespace Lidarr.Api.V1.TrackFiles
private Response SetQuality()
{
var resource = Request.Body.FromJson<TrackFileListResource>();
var trackFiles = _mediaFileService.GetFiles(resource.TrackFileIds);
var trackFiles = _mediaFileService.Get(resource.TrackFileIds);
foreach (var trackFile in trackFiles)
{
@ -130,9 +126,7 @@ namespace Lidarr.Api.V1.TrackFiles
_mediaFileService.Update(trackFiles);
var artist = _artistService.GetArtist(trackFiles.First().ArtistId);
return trackFiles.ConvertAll(f => f.ToResource(artist, _upgradableSpecification))
return trackFiles.ConvertAll(f => f.ToResource(trackFiles.First().Artist.Value, _upgradableSpecification))
.AsResponse(Nancy.HttpStatusCode.Accepted);
}
@ -145,7 +139,7 @@ namespace Lidarr.Api.V1.TrackFiles
throw new NzbDroneClientException(HttpStatusCode.NotFound, "Track file not found");
}
var artist = _artistService.GetArtist(trackFile.ArtistId);
var artist = trackFile.Artist.Value;
var fullPath = Path.Combine(artist.Path, trackFile.RelativePath);
_mediaFileDeletionService.DeleteTrackFile(artist, trackFile);
@ -154,8 +148,8 @@ namespace Lidarr.Api.V1.TrackFiles
private Response DeleteTrackFiles()
{
var resource = Request.Body.FromJson<TrackFileListResource>();
var trackFiles = _mediaFileService.GetFiles(resource.TrackFileIds);
var artist = _artistService.GetArtist(trackFiles.First().ArtistId);
var trackFiles = _mediaFileService.Get(resource.TrackFileIds);
var artist = trackFiles.First().Artist.Value;
foreach (var trackFile in trackFiles)
{
@ -169,12 +163,12 @@ namespace Lidarr.Api.V1.TrackFiles
public void Handle(TrackFileAddedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id);
BroadcastResourceChange(ModelAction.Updated, message.TrackFile.ToResource(message.TrackFile.Artist.Value, _upgradableSpecification));
}
public void Handle(TrackFileDeletedEvent message)
{
BroadcastResourceChange(ModelAction.Deleted, message.TrackFile.Id);
BroadcastResourceChange(ModelAction.Deleted, message.TrackFile.ToResource(message.TrackFile.Artist.Value, _upgradableSpecification));
}
}

@ -36,7 +36,7 @@ namespace Lidarr.Api.V1.TrackFiles
{
Id = model.Id,
ArtistId = model.ArtistId,
ArtistId = model.Artist.Value.Id,
AlbumId = model.AlbumId,
RelativePath = model.RelativePath,
//Path
@ -59,7 +59,7 @@ namespace Lidarr.Api.V1.TrackFiles
{
Id = model.Id,
ArtistId = model.ArtistId,
ArtistId = artist.Id,
AlbumId = model.AlbumId,
RelativePath = model.RelativePath,
Path = Path.Combine(artist.Path, model.RelativePath),

@ -25,11 +25,12 @@ namespace Lidarr.Api.V1.Tracks
{
var artistIdQuery = Request.Query.ArtistId;
var albumIdQuery = Request.Query.AlbumId;
var albumReleaseIdQuery = Request.Query.AlbumReleaseId;
var trackIdsQuery = Request.Query.TrackIds;
if (!artistIdQuery.HasValue && !trackIdsQuery.HasValue && !albumIdQuery.HasValue)
if (!artistIdQuery.HasValue && !trackIdsQuery.HasValue && !albumIdQuery.HasValue && !albumReleaseIdQuery.HasValue)
{
throw new BadRequestException("artistId or trackIds must be provided");
throw new BadRequestException("One of artistId, albumId, albumReleaseId or trackIds must be provided");
}
if (artistIdQuery.HasValue && !albumIdQuery.HasValue)
@ -39,6 +40,13 @@ namespace Lidarr.Api.V1.Tracks
return MapToResource(_trackService.GetTracksByArtist(artistId), false, false);
}
if (albumReleaseIdQuery.HasValue)
{
int releaseId = Convert.ToInt32(albumReleaseIdQuery.Value);
return MapToResource(_trackService.GetTracksByRelease(releaseId), false, false);
}
if (albumIdQuery.HasValue)
{
int albumId = Convert.ToInt32(albumIdQuery.Value);

@ -107,7 +107,8 @@ namespace Lidarr.Api.V1.Tracks
{
foreach (var track in message.TrackInfo.Tracks)
{
BroadcastResourceChange(ModelAction.Updated, track.Id);
track.TrackFile = message.ImportedTrack;
BroadcastResourceChange(ModelAction.Updated, MapToResource(track, true, true));
}
}
@ -115,7 +116,8 @@ namespace Lidarr.Api.V1.Tracks
{
foreach (var track in message.TrackFile.Tracks.Value)
{
BroadcastResourceChange(ModelAction.Updated, track.Id);
track.TrackFile = message.TrackFile;
BroadcastResourceChange(ModelAction.Updated, MapToResource(track, true, true));
}
}

@ -41,7 +41,7 @@ namespace Lidarr.Api.V1.Tracks
{
Id = model.Id,
ArtistId = model.ArtistId,
ArtistId = model.Artist.Value.Id,
TrackFileId = model.TrackFileId,
AlbumId = model.AlbumId,
Explicit = model.Explicit,

@ -165,7 +165,7 @@ namespace Marr.Data
/// </summary>
public void AddLazyRelationship(Relationship childRelationship)
{
_children.Add(new EntityGraph(childRelationship.RelationshipInfo.EntityType.GetGenericArguments()[0], this, childRelationship));
_children.Add(new EntityGraph(childRelationship.RelationshipInfo.EntityType, this, childRelationship));
}
/// <summary>
@ -297,16 +297,30 @@ namespace Marr.Data
private void InitOneToManyChildLists(EntityReference entityRef)
{
// Get a reference to the parent's the childrens' OwningLists to the parent entity
for (int i = 0; i < Relationships.Count; i++)
foreach (var child in _children)
{
Relationship relationship = Relationships[i];
Relationship relationship = child._relationship;
if (relationship.RelationshipInfo.RelationType == RelationshipTypes.Many)
{
try
{
IList list = (IList)_repos.ReflectionStrategy.CreateInstance(relationship.MemberType);
relationship.Setter(entityRef.Entity, list);
var memberType = relationship.MemberType;
object memberInstance;
object childList;
if (typeof(ILazyLoaded).IsAssignableFrom(memberType))
{
childList = _repos.ReflectionStrategy.CreateInstance(memberType.GetGenericArguments()[0]);
memberInstance = Activator.CreateInstance(relationship.MemberType, childList);
}
else
{
childList = _repos.ReflectionStrategy.CreateInstance(memberType);
memberInstance = childList;
}
IList list = (IList) childList;
relationship.Setter(entityRef.Entity, memberInstance);
// Save a reference to each 1-M list
entityRef.AddChildList(relationship.Member.Name, list);
}

@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Linq;
using System.Linq.Expressions;
using Marr.Data.Mapping.Strategies;
@ -65,8 +66,21 @@ namespace Marr.Data.Mapping
public RelationshipBuilder<TEntity> LazyLoad<TChild>(Func<IDataMapper, TEntity, TChild> query, Func<TEntity, bool> condition = null)
{
AssertCurrentPropertyIsSet();
var relationship = Relationships[_currentPropertyName];
Relationships[_currentPropertyName].LazyLoaded = new LazyLoaded<TEntity, TChild>(query, condition);
relationship.LazyLoaded = new LazyLoaded<TEntity, TChild>(query, condition);
// work out if it's one to many or not
if (typeof(ICollection).IsAssignableFrom(typeof(TChild)))
{
relationship.RelationshipInfo.RelationType = RelationshipTypes.Many;
relationship.RelationshipInfo.EntityType = typeof(TChild).GetGenericArguments()[0];
}
else
{
relationship.RelationshipInfo.EntityType = typeof(TChild);
}
return this;
}

@ -568,6 +568,23 @@ namespace Marr.Data.QGen
return Join(joinType, rightMember, filterExpression);
}
public virtual QueryBuilder<T> Join<TLeft, TRight>(JoinType joinType, Expression<Func<TLeft, LazyLoaded<List<TRight>>>> rightEntity, Expression<Func<TLeft, TRight, bool>> filterExpression)
{
_isJoin = true;
MemberInfo rightMember = (rightEntity.Body as MemberExpression).Member;
foreach (var item in EntGraph)
{
if (item.EntityType == typeof(TLeft))
{
var relationship = item.Relationships.Single(v => v.Member == rightMember);
item.AddLazyRelationship(relationship);
}
}
return Join(joinType, rightMember, filterExpression);
}
public virtual QueryBuilder<T> Join<TLeft, TRight>(JoinType joinType, MemberInfo rightMember, Expression<Func<TLeft, TRight, bool>> filterExpression)
{
_isJoin = true;

@ -102,6 +102,32 @@ namespace NzbDrone.Common.Test.CacheTests
hitCount.Should().BeInRange(3, 6);
}
[Test]
public void should_clear_expired_when_they_expire()
{
int hitCount = 0;
Func<string> testfunc = () => {
hitCount++;
return null;
};
_cachedString.Values.Should().HaveCount(0);
_cachedString.Get("key", testfunc, TimeSpan.FromMilliseconds(300));
Thread.Sleep(100);
_cachedString.Values.Should().HaveCount(1);
_cachedString.Get("key", testfunc, TimeSpan.FromMilliseconds(300));
Thread.Sleep(1000);
_cachedString.Values.Should().HaveCount(0);
hitCount.Should().Be(1);
}
}
public class Worker
@ -114,4 +140,4 @@ namespace NzbDrone.Common.Test.CacheTests
return "Hit count is " + HitCount;
}
}
}
}

@ -40,6 +40,11 @@ namespace NzbDrone.Common.Cache
{
Ensure.That(key, () => key).IsNotNullOrWhiteSpace();
_store[key] = new CacheItem(value, lifetime);
if (lifetime != null)
{
System.Threading.Tasks.Task.Delay(lifetime.Value).ContinueWith(t => _store.TryRemove(key, out var temp));
}
}
public T Find(string key)

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace NzbDrone.Common.Extensions
{
@ -51,6 +52,34 @@ namespace NzbDrone.Common.Extensions
}
}
public static TSource ExclusiveOrDefault<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
var results = source.Take(2).ToArray();
return results.Length == 1 ? results[0] : default(TSource);
}
public static TSource ExclusiveOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if (predicate == null)
{
throw new ArgumentNullException("predicate");
}
var results = source.Where(predicate).Take(2).ToArray();
return results.Length == 1 ? results[0] : default(TSource);
}
public static Dictionary<TKey, TItem> ToDictionaryIgnoreDuplicates<TItem, TKey>(this IEnumerable<TItem> src, Func<TItem, TKey> keySelector)
{
var result = new Dictionary<TKey, TItem>();

@ -16,33 +16,27 @@ namespace NzbDrone.Core.Test.Datastore
[Test]
public void one_to_one()
{
var trackFile = Builder<TrackFile>.CreateNew()
var album = Builder<Album>.CreateNew()
.With(c => c.Id = 0)
.With(c => c.Quality = new QualityModel())
.With(c => c.Language = Language.English)
.BuildNew();
Db.Insert(album);
Db.Insert(trackFile);
var track = Builder<Track>.CreateNew()
var albumRelease = Builder<AlbumRelease>.CreateNew()
.With(c => c.Id = 0)
.With(c => c.TrackFileId = trackFile.Id)
.With(c => c.AlbumId = album.Id)
.BuildNew();
Db.Insert(track);
var loadedTrackFile = Db.Single<Track>().TrackFile.Value;
loadedTrackFile.Should().NotBeNull();
loadedTrackFile.ShouldBeEquivalentTo(trackFile,
options => options
.IncludingAllRuntimeProperties()
.Excluding(c => c.DateAdded)
.Excluding(c => c.Path)
.Excluding(c => c.Artist)
.Excluding(c => c.Tracks)
.Excluding(c => c.Album)
.Excluding(c => c.ArtistId));
Db.Insert(albumRelease);
var loadedAlbum = Db.Single<AlbumRelease>().Album.Value;
loadedAlbum.Should().NotBeNull();
loadedAlbum.ShouldBeEquivalentTo(album,
options => options
.IncludingAllRuntimeProperties()
.Excluding(c => c.Artist)
.Excluding(c => c.ArtistId)
.Excluding(c => c.ArtistMetadata)
.Excluding(c => c.AlbumReleases));
}
[Test]

@ -11,6 +11,7 @@ using NzbDrone.Core.Profiles.Languages;
using NzbDrone.Core.Test.Languages;
using Marr.Data.QGen;
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.Test.Datastore
{
@ -78,6 +79,7 @@ namespace NzbDrone.Core.Test.Datastore
var trackFiles = Builder<TrackFile>.CreateListOfSize(1)
.All()
.With(v => v.Id = 0)
.With(v => v.AlbumId = albums[0].Id)
.With(v => v.Quality = new QualityModel())
.BuildListOfNew();
@ -149,6 +151,141 @@ namespace NzbDrone.Core.Test.Datastore
}
}
[Test]
public void should_lazy_load_artist_for_track()
{
var db = Mocker.Resolve<IDatabase>();
var DataMapper = db.GetDataMapper();
var tracks = DataMapper.Query<Track>()
.ToList();
Assert.IsNotEmpty(tracks);
foreach (var track in tracks)
{
Assert.IsFalse(track.Artist.IsLoaded);
Assert.IsNotNull(track.Artist.Value);
Assert.IsTrue(track.Artist.IsLoaded);
Assert.IsTrue(track.Artist.Value.Metadata.IsLoaded);
}
}
[Test]
public void should_lazy_load_artist_for_trackfile()
{
var db = Mocker.Resolve<IDatabase>();
var DataMapper = db.GetDataMapper();
var tracks = DataMapper.Query<TrackFile>()
.ToList();
Assert.IsNotEmpty(tracks);
foreach (var track in tracks)
{
Assert.IsFalse(track.Artist.IsLoaded);
Assert.IsNotNull(track.Artist.Value);
Assert.IsTrue(track.Artist.IsLoaded);
Assert.IsTrue(track.Artist.Value.Metadata.IsLoaded);
}
}
[Test]
public void should_lazy_load_trackfile_if_not_joined()
{
var db = Mocker.Resolve<IDatabase>();
var DataMapper = db.GetDataMapper();
var tracks = DataMapper.Query<Track>()
.ToList();
foreach (var track in tracks)
{
Assert.IsFalse(track.TrackFile.IsLoaded);
Assert.IsNotNull(track.TrackFile.Value);
Assert.IsTrue(track.TrackFile.IsLoaded);
}
}
[Test]
public void should_explicit_load_everything_if_joined()
{
var db = Mocker.Resolve<IDatabase>();
var DataMapper = db.GetDataMapper();
var files = DataMapper.Query<TrackFile>()
.Join<TrackFile, Track>(JoinType.Inner, f => f.Tracks, (f, t) => f.Id == t.TrackFileId)
.Join<TrackFile, Album>(JoinType.Inner, t => t.Album, (t, a) => t.AlbumId == a.Id)
.Join<TrackFile, Artist>(JoinType.Inner, t => t.Artist, (t, a) => t.Album.Value.ArtistMetadataId == a.ArtistMetadataId)
.Join<Artist, ArtistMetadata>(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id)
.ToList();
Assert.IsNotEmpty(files);
foreach (var file in files)
{
Assert.IsTrue(file.Tracks.IsLoaded);
Assert.IsNotEmpty(file.Tracks.Value);
Assert.IsTrue(file.Album.IsLoaded);
Assert.IsTrue(file.Artist.IsLoaded);
Assert.IsTrue(file.Artist.Value.Metadata.IsLoaded);
}
}
[Test]
public void should_lazy_load_tracks_if_not_joined_to_trackfile()
{
var db = Mocker.Resolve<IDatabase>();
var DataMapper = db.GetDataMapper();
var files = DataMapper.Query<TrackFile>()
.Join<TrackFile, Album>(JoinType.Inner, t => t.Album, (t, a) => t.AlbumId == a.Id)
.Join<TrackFile, Artist>(JoinType.Inner, t => t.Artist, (t, a) => t.Album.Value.ArtistMetadataId == a.ArtistMetadataId)
.Join<Artist, ArtistMetadata>(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id)
.ToList();
Assert.IsNotEmpty(files);
foreach (var file in files)
{
Assert.IsFalse(file.Tracks.IsLoaded);
Assert.IsNotNull(file.Tracks.Value);
Assert.IsNotEmpty(file.Tracks.Value);
Assert.IsTrue(file.Tracks.IsLoaded);
Assert.IsTrue(file.Album.IsLoaded);
Assert.IsTrue(file.Artist.IsLoaded);
Assert.IsTrue(file.Artist.Value.Metadata.IsLoaded);
}
}
[Test]
public void should_lazy_load_tracks_if_not_joined()
{
var db = Mocker.Resolve<IDatabase>();
var DataMapper = db.GetDataMapper();
var release = DataMapper.Query<AlbumRelease>().Where(x => x.Id == 1).SingleOrDefault();
Assert.IsFalse(release.Tracks.IsLoaded);
Assert.IsNotNull(release.Tracks.Value);
Assert.IsNotEmpty(release.Tracks.Value);
Assert.IsTrue(release.Tracks.IsLoaded);
}
[Test]
public void should_lazy_load_track_if_not_joined()
{
var db = Mocker.Resolve<IDatabase>();
var DataMapper = db.GetDataMapper();
var tracks = DataMapper.Query<TrackFile>()
.ToList();
foreach (var track in tracks)
{
Assert.IsFalse(track.Tracks.IsLoaded);
Assert.IsNotNull(track.Tracks.Value);
Assert.IsTrue(track.Tracks.IsLoaded);
}
}
[Test]
public void should_explicit_load_profile_if_joined()
{

@ -44,6 +44,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = languages })
.Build();
Mocker.GetMock<ITrackService>()
.Setup(c => c.TracksWithoutFiles(It.IsAny<int>()))
.Returns(new List<Track>());
Mocker.GetMock<IMediaFileService>()
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>()))
.Returns(new List<TrackFile> { _firstFile, _secondFile });
@ -84,6 +88,26 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_if_track_is_missing()
{
Mocker.GetMock<ITrackService>()
.Setup(c => c.TracksWithoutFiles(It.IsAny<int>()))
.Returns(new List<Track> { new Track() });
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
}
[Test]
public void should_only_query_db_for_missing_tracks_once()
{
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
Mocker.GetMock<ITrackService>()
.Verify(c => c.TracksWithoutFiles(It.IsAny<int>()), Times.Once());
}
[Test]
public void should_return_true_if_single_album_doesnt_exist_on_disk()
{

@ -17,6 +17,7 @@ using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Test.Common;
using NzbDrone.Core.MediaFiles.Events;
namespace NzbDrone.Core.Test.Download
{
@ -61,12 +62,25 @@ namespace NzbDrone.Core.Test.Download
}
private Album CreateAlbum(int id, int trackCount)
{
return new Album {
Id = id,
AlbumReleases = new List<AlbumRelease> {
new AlbumRelease {
Monitored = true,
TrackCount = trackCount
}
}
};
}
private RemoteAlbum BuildRemoteAlbum()
{
return new RemoteAlbum
{
Artist = new Artist(),
Albums = new List<Album> { new Album { Id = 1 } }
Albums = new List<Album> { CreateAlbum(1, 1) }
};
}
@ -84,13 +98,14 @@ namespace NzbDrone.Core.Test.Download
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalTrack() { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }))
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }))
});
}
private void GivenABadlyNamedDownload()
{
_trackedDownload.RemoteAlbum.Artist = null;
_trackedDownload.DownloadItem.DownloadId = "1234";
_trackedDownload.DownloadItem.Title = "Droned Pilot"; // Set a badly named download
Mocker.GetMock<IHistoryService>()
@ -162,18 +177,49 @@ namespace NzbDrone.Core.Test.Download
}
[Test]
public void should_mark_as_imported_if_all_episodes_were_imported()
public void should_mark_as_imported_if_all_tracks_were_imported()
{
_trackedDownload.RemoteAlbum.Albums = new List<Album>
{
CreateAlbum(1, 2)
};
Mocker.GetMock<IDownloadedTracksImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision<LocalTrack>(
new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(
new ImportDecision<LocalTrack>(
new LocalTrack {Path = @"C:\TestPath\Droned.S01E02.mkv".AsOsAgnostic()}))
});
Subject.Process(_trackedDownload);
AssertCompletedDownload();
}
[Test]
public void should_mark_as_imported_if_all_tracks_were_imported_but_album_incomplete()
{
_trackedDownload.RemoteAlbum.Albums = new List<Album>
{
CreateAlbum(1, 3)
};
Mocker.GetMock<IDownloadedTracksImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new ImportDecision<LocalTrack>(
new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(
new ImportDecision(
new ImportDecision<LocalTrack>(
new LocalTrack {Path = @"C:\TestPath\Droned.S01E02.mkv".AsOsAgnostic()}))
});
@ -190,11 +236,11 @@ namespace NzbDrone.Core.Test.Download
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new ImportDecision<LocalTrack>(
new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}, new Rejection("Rejected!")), "Test Failure"),
new ImportResult(
new ImportDecision(
new ImportDecision<LocalTrack>(
new LocalTrack {Path = @"C:\TestPath\Droned.S01E02.mkv".AsOsAgnostic()},new Rejection("Rejected!")), "Test Failure")
});
@ -203,22 +249,22 @@ namespace NzbDrone.Core.Test.Download
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent<DownloadCompletedEvent>(It.IsAny<DownloadCompletedEvent>()), Times.Never());
AssertNoCompletedDownload();
AssertImportIncomplete();
}
[Test]
public void should_not_mark_as_imported_if_no_episodes_were_parsed()
public void should_not_mark_as_imported_if_no_tracks_were_parsed()
{
Mocker.GetMock<IDownloadedTracksImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new ImportDecision<LocalTrack>(
new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}, new Rejection("Rejected!")), "Test Failure"),
new ImportResult(
new ImportDecision(
new ImportDecision<LocalTrack>(
new LocalTrack {Path = @"C:\TestPath\Droned.S01E02.mkv".AsOsAgnostic()},new Rejection("Rejected!")), "Test Failure")
});
@ -226,7 +272,7 @@ namespace NzbDrone.Core.Test.Download
Subject.Process(_trackedDownload);
AssertNoCompletedDownload();
AssertImportIncomplete();
}
[Test]
@ -236,32 +282,34 @@ namespace NzbDrone.Core.Test.Download
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}),"Test Failure"),
new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}),"Test Failure")
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}),"Test Failure"),
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}),"Test Failure")
});
Subject.Process(_trackedDownload);
AssertNoCompletedDownload();
AssertImportIncomplete();
}
[Test]
public void should_mark_as_imported_if_all_episodes_were_imported_but_extra_files_were_not()
public void should_mark_as_imported_if_all_tracks_were_imported_but_extra_files_were_not()
{
GivenArtistMatch();
_trackedDownload.RemoteAlbum.Albums = new List<Album>
{
new Album()
CreateAlbum(1, 3)
};
Mocker.GetMock<IDownloadedTracksImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(new ImportDecision(new LocalTrack{Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}),"Test Failure")
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}), "Test Failure")
});
Subject.Process(_trackedDownload);
@ -270,28 +318,30 @@ namespace NzbDrone.Core.Test.Download
}
[Test]
public void should_mark_as_failed_if_some_of_episodes_were_not_imported()
public void should_mark_as_failed_if_some_tracks_were_not_imported()
{
_trackedDownload.RemoteAlbum.Albums = new List<Album>
{
new Album(),
new Album(),
new Album()
CreateAlbum(1, 1),
CreateAlbum(1, 2),
CreateAlbum(1, 1)
};
Mocker.GetMock<IDownloadedTracksImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(new ImportDecision(new LocalTrack{Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}),"Test Failure"),
new ImportResult(new ImportDecision(new LocalTrack{Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}),"Test Failure")
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()})),
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}), "Test Failure"),
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}), "Test Failure")
});
Subject.Process(_trackedDownload);
AssertNoCompletedDownload();
AssertImportIncomplete();
}
[Test]
@ -303,7 +353,7 @@ namespace NzbDrone.Core.Test.Download
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}))
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}))
});
Mocker.GetMock<IArtistService>()
@ -324,7 +374,7 @@ namespace NzbDrone.Core.Test.Download
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}))
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}))
});
Mocker.GetMock<IHistoryService>()
@ -338,6 +388,7 @@ namespace NzbDrone.Core.Test.Download
[Test]
public void should_not_import_when_there_is_a_title_mismatch()
{
_trackedDownload.RemoteAlbum.Artist = null;
Mocker.GetMock<IParsingService>()
.Setup(s => s.GetArtist("Drone.S01E01.HDTV"))
.Returns((Artist)null);
@ -352,14 +403,14 @@ namespace NzbDrone.Core.Test.Download
{
_trackedDownload.RemoteAlbum.Albums = new List<Album>
{
new Album()
CreateAlbum(0, 1)
};
Mocker.GetMock<IDownloadedTracksImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}))
new ImportResult(new ImportDecision<LocalTrack>(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic()}))
});
Subject.Process(_trackedDownload, true);
@ -399,6 +450,14 @@ namespace NzbDrone.Core.Test.Download
AssertNoCompletedDownload();
}
private void AssertImportIncomplete()
{
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<AlbumImportIncompleteEvent>()), Times.Once());
AssertNoCompletedDownload();
}
private void AssertNoCompletedDownload()
{
Mocker.GetMock<IEventAggregator>()

@ -0,0 +1,194 @@
{
"expectedMusicBrainzReleaseIds": [
"0ce2d66f-e871-415a-9a85-e564f99d4021"
],
"metadataProfile": {
"name": "Standard",
"primaryAlbumTypes": [
{
"primaryAlbumType": {
"id": 2,
"name": "Single"
},
"allowed": true
},
{
"primaryAlbumType": {
"id": 4,
"name": "Other"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 1,
"name": "EP"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 3,
"name": "Broadcast"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 0,
"name": "Album"
},
"allowed": true
}
],
"secondaryAlbumTypes": [
{
"secondaryAlbumType": {
"id": 0,
"name": "Studio"
},
"allowed": true
},
{
"secondaryAlbumType": {
"id": 3,
"name": "Spokenword"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 2,
"name": "Soundtrack"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 7,
"name": "Remix"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 9,
"name": "Mixtape/Street"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 6,
"name": "Live"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 4,
"name": "Interview"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 8,
"name": "DJ-mix"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 10,
"name": "Demo"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 1,
"name": "Compilation"
},
"allowed": false
}
],
"releaseStatuses": [
{
"releaseStatus": {
"id": 3,
"name": "Pseudo"
},
"allowed": false
},
{
"releaseStatus": {
"id": 1,
"name": "Promotion"
},
"allowed": false
},
{
"releaseStatus": {
"id": 0,
"name": "Official"
},
"allowed": true
},
{
"releaseStatus": {
"id": 2,
"name": "Bootleg"
},
"allowed": false
}
],
"id": 1
},
"artist": "7ac055fa-e357-4890-9098-010b8094a900",
"newDownload": false,
"singleRelease": false,
"tracks": [
{
"path": "D:\\Test2\\Alabama\\The Touch\\06 Touch Me When We're Dancing.mp3",
"fileTrackInfo": {
"title": "Touch Me When We're Dancing",
"cleanTitle": "Touch Me When We're Dancing",
"artistTitle": "Alabama",
"albumTitle": "The Touch",
"artistTitleInfo": {
"title": "Alabama",
"year": 1986
},
"discNumber": 0,
"discCount": 0,
"year": 1986,
"duration": "00:03:43.2950000",
"quality": {
"quality": {
"id": 2,
"name": "MP3-VBR-V0"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3 VBR",
"audioBitrate": 161,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
6
],
"language": {
"id": 1,
"name": "English"
}
}
}
]
}

@ -0,0 +1,235 @@
{
"expectedMusicBrainzReleaseIds": [
"25f0fa1b-ae04-479a-a182-18a655ff6040"
],
"metadataProfile": {
"name": "Album+Single",
"primaryAlbumTypes": [
{
"primaryAlbumType": {
"id": 4,
"name": "Other"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 3,
"name": "Broadcast"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 2,
"name": "Single"
},
"allowed": true
},
{
"primaryAlbumType": {
"id": 1,
"name": "EP"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 0,
"name": "Album"
},
"allowed": true
}
],
"secondaryAlbumTypes": [
{
"secondaryAlbumType": {
"id": 10,
"name": "Demo"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 9,
"name": "Mixtape/Street"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 8,
"name": "DJ-mix"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 7,
"name": "Remix"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 6,
"name": "Live"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 4,
"name": "Interview"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 3,
"name": "Spokenword"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 2,
"name": "Soundtrack"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 1,
"name": "Compilation"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 0,
"name": "Studio"
},
"allowed": true
}
],
"releaseStatuses": [
{
"releaseStatus": {
"id": 3,
"name": "Pseudo"
},
"allowed": false
},
{
"releaseStatus": {
"id": 2,
"name": "Bootleg"
},
"allowed": false
},
{
"releaseStatus": {
"id": 1,
"name": "Promotion"
},
"allowed": false
},
{
"releaseStatus": {
"id": 0,
"name": "Official"
},
"allowed": true
}
],
"id": 2
},
"artist": "70248960-cb53-4ea4-943a-edb18f7d336f",
"newDownload": true,
"singleRelease": false,
"tracks": [
{
"path": "/mnt/data1/LidarrTest/Bruce Springsteen/Album/10_Glory_Days.mp3",
"fileTrackInfo": {
"title": "Glory Days",
"cleanTitle": "Glory Days",
"artistTitle": "Bruce Springsteen",
"albumTitle": "Born in the U.S.A.",
"artistTitleInfo": {
"title": "Bruce Springsteen",
"year": 1984
},
"discNumber": 0,
"discCount": 0,
"year": 1984,
"duration": "00:04:18.0680000",
"quality": {
"quality": {
"id": 1,
"name": "MP3-192"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 192,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
10
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Bruce Springsteen/Album/11_Dancing_In_The_Dark.mp3",
"fileTrackInfo": {
"title": "Dancing In The Dark",
"cleanTitle": "Dancing In The Dark",
"artistTitle": "Bruce Springsteen",
"albumTitle": "Born in the U.S.A.",
"artistTitleInfo": {
"title": "Bruce Springsteen",
"year": 1984
},
"discNumber": 0,
"discCount": 0,
"year": 1984,
"duration": "00:04:03.0450000",
"quality": {
"quality": {
"id": 1,
"name": "MP3-192"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 192,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
11
],
"language": {
"id": 1,
"name": "English"
}
}
}
]
}

@ -0,0 +1,573 @@
{
"expectedMusicBrainzReleaseIds": [
"4e2dd34f-53fe-4d54-b564-b14a2871505e"
],
"metadataProfile": {
"name": "Standard",
"primaryAlbumTypes": [
{
"primaryAlbumType": {
"id": 2,
"name": "Single"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 4,
"name": "Other"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 1,
"name": "EP"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 3,
"name": "Broadcast"
},
"allowed": false
},
{
"primaryAlbumType": {
"id": 0,
"name": "Album"
},
"allowed": true
}
],
"secondaryAlbumTypes": [
{
"secondaryAlbumType": {
"id": 0,
"name": "Studio"
},
"allowed": true
},
{
"secondaryAlbumType": {
"id": 3,
"name": "Spokenword"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 2,
"name": "Soundtrack"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 7,
"name": "Remix"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 9,
"name": "Mixtape/Street"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 6,
"name": "Live"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 4,
"name": "Interview"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 8,
"name": "DJ-mix"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 10,
"name": "Demo"
},
"allowed": false
},
{
"secondaryAlbumType": {
"id": 1,
"name": "Compilation"
},
"allowed": false
}
],
"releaseStatuses": [
{
"releaseStatus": {
"id": 3,
"name": "Pseudo"
},
"allowed": false
},
{
"releaseStatus": {
"id": 1,
"name": "Promotion"
},
"allowed": false
},
{
"releaseStatus": {
"id": 0,
"name": "Official"
},
"allowed": true
},
{
"releaseStatus": {
"id": 2,
"name": "Bootleg"
},
"allowed": false
}
],
"id": 1
},
"artist": "6fe07aa5-fec0-4eca-a456-f29bff451b04",
"newDownload": false,
"singleRelease": false,
"tracks": [
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/01-weezer-africa-0f640cbf.mp3",
"fileTrackInfo": {
"title": "Africa",
"cleanTitle": "Africa",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:03:58.6850000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
1
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/02-weezer-everybody_wants_to_rule_the_world-efc2a5b4.mp3",
"fileTrackInfo": {
"title": "Everybody Wants To Rule The World",
"cleanTitle": "Everybody Wants To Rule The World",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:04:04.8960000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
2
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/03-weezer-sweet_dreams_(are_made_of_this)-a8a934a6.mp3",
"fileTrackInfo": {
"title": "Sweet Dreams (Are Made Of This)",
"cleanTitle": "Sweet Dreams (Are Made Of This)",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:03:34.8550000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
3
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/04-weezer-take_on_me-5698a04c.mp3",
"fileTrackInfo": {
"title": "Take On Me",
"cleanTitle": "Take On Me",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:03:43.6510000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
4
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/05-weezer-happy_together-dd30d8d4.mp3",
"fileTrackInfo": {
"title": "Happy Together",
"cleanTitle": "Happy Together",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:02:25.7160000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
5
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/06-weezer-paranoid-d0617671.mp3",
"fileTrackInfo": {
"title": "Paranoid",
"cleanTitle": "Paranoid",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:02:44.9260000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
6
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/07-weezer-mr_blue_sky-e3e44f02.mp3",
"fileTrackInfo": {
"title": "Mr. Blue Sky",
"cleanTitle": "Mr. Blue Sky",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:04:46.4210000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
7
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/08-weezer-no_scrubs-577fa9fa.mp3",
"fileTrackInfo": {
"title": "No Scrubs",
"cleanTitle": "No Scrubs",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:03:10.3730000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
8
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/09-weezer-billie_jean-9c35bbda.mp3",
"fileTrackInfo": {
"title": "Billie Jean",
"cleanTitle": "Billie Jean",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:04:54.1990000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
9
],
"language": {
"id": 1,
"name": "English"
}
}
},
{
"path": "/mnt/data1/LidarrTest/Weezer/Weezer (2019)/10-weezer-stand_by_me-396f336f.mp3",
"fileTrackInfo": {
"title": "Stand By Me",
"cleanTitle": "Stand By Me",
"artistTitle": "Weezer",
"albumTitle": "Weezer (Teal Album)",
"artistTitleInfo": {
"title": "Weezer",
"year": 2019
},
"discNumber": 0,
"discCount": 0,
"year": 2019,
"label": "Crush Music / Atlantic ",
"duration": "00:03:00.9770000",
"quality": {
"quality": {
"id": 4,
"name": "MP3-320"
},
"revision": {
"version": 1,
"real": 0
}
},
"mediaInfo": {
"audioFormat": "MPEG Version 1 Audio, Layer 3",
"audioBitrate": 320,
"audioChannels": 2,
"audioBits": 0,
"audioSampleRate": 44100
},
"trackNumbers": [
10
],
"language": {
"id": 1,
"name": "English"
}
}
}
]
}

@ -12,6 +12,7 @@ using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Core.RootFolders;
using NzbDrone.Test.Common;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
{
@ -44,6 +45,14 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Mocker.GetMock<IRootFolderService>()
.Setup(s => s.GetBestRootFolderPath(It.IsAny<string>()))
.Returns(_rootFolder);
Mocker.GetMock<IMakeImportDecision>()
.Setup(v => v.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>()))
.Returns(new List<ImportDecision<LocalTrack>>());
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(It.IsAny<int>()))
.Returns(new List<TrackFile>());
}
private void GivenRootFolder(params string[] subfolders)

@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(true);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
var downloadItem = Builder<DownloadClientItem>.CreateNew()
@ -76,15 +76,15 @@ namespace NzbDrone.Core.Test.MediaFiles
{
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var imported = new List<ImportDecision<LocalTrack>>();
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), It.IsAny<bool>(), It.IsAny<DownloadClientItem>(), It.IsAny<ImportMode>()))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), It.IsAny<bool>(), It.IsAny<DownloadClientItem>(), It.IsAny<ImportMode>()))
.Returns(imported.Select(i => new ImportResult(i)).ToList())
.Callback(() => WasImportedResponse());
}
@ -155,7 +155,7 @@ namespace NzbDrone.Core.Test.MediaFiles
public void should_not_delete_folder_if_no_files_were_imported()
{
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), false, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
@ -171,15 +171,15 @@ namespace NzbDrone.Core.Test.MediaFiles
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var imported = new List<ImportDecision<LocalTrack>>();
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
@ -225,8 +225,8 @@ namespace NzbDrone.Core.Test.MediaFiles
result.Should().HaveCount(1);
result.First().ImportDecision.Should().NotBeNull();
result.First().ImportDecision.LocalTrack.Should().NotBeNull();
result.First().ImportDecision.LocalTrack.Path.Should().Be(fileName);
result.First().ImportDecision.Item.Should().NotBeNull();
result.First().ImportDecision.Item.Path.Should().Be(fileName);
result.First().Result.Should().Be(ImportResultType.Rejected);
}
@ -237,15 +237,15 @@ namespace NzbDrone.Core.Test.MediaFiles
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var imported = new List<ImportDecision<LocalTrack>>();
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
//Mocker.GetMock<IDetectSample>()
@ -272,56 +272,6 @@ namespace NzbDrone.Core.Test.MediaFiles
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_use_folder_if_folder_import()
{
GivenValidArtist();
var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic();
var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists(folderName))
.Returns(true);
Mocker.GetMock<IDiskProvider>().Setup(c => c.GetFiles(folderName, SearchOption.TopDirectoryOnly))
.Returns(new[] { fileName });
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
Subject.ProcessPath(fileName);
Mocker.GetMock<IMakeImportDecision>()
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), It.Is<ParsedTrackInfo>(v => v.TrackNumbers.First() == 9)), Times.Once());
}
[Test]
public void should_not_use_folder_if_file_import()
{
GivenValidArtist();
var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\Torrents\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists(fileName))
.Returns(false);
Mocker.GetMock<IDiskProvider>().Setup(c => c.FileExists(fileName))
.Returns(true);
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var result = Subject.ProcessPath(fileName);
Mocker.GetMock<IMakeImportDecision>()
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null), Times.Once());
}
[Test]
public void should_not_process_if_file_and_folder_do_not_exist()
{
@ -348,15 +298,15 @@ namespace NzbDrone.Core.Test.MediaFiles
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var imported = new List<ImportDecision<LocalTrack>>();
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
//Mocker.GetMock<IDetectSample>()
@ -424,13 +374,13 @@ namespace NzbDrone.Core.Test.MediaFiles
private void VerifyNoImport()
{
Mocker.GetMock<IImportApprovedTracks>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
Mocker.GetMock<IImportApprovedTracks>().Verify(c => c.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto),
Times.Never());
}
private void VerifyImport()
{
Mocker.GetMock<IImportApprovedTracks>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
Mocker.GetMock<IImportApprovedTracks>().Verify(c => c.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto),
Times.Once());
}
}

@ -25,16 +25,16 @@ namespace NzbDrone.Core.Test.MediaFiles
[TestFixture]
public class ImportApprovedTracksFixture : CoreTest<ImportApprovedTracks>
{
private List<ImportDecision> _rejectedDecisions;
private List<ImportDecision> _approvedDecisions;
private List<ImportDecision<LocalTrack>> _rejectedDecisions;
private List<ImportDecision<LocalTrack>> _approvedDecisions;
private DownloadClientItem _downloadClientItem;
[SetUp]
public void Setup()
{
_rejectedDecisions = new List<ImportDecision>();
_approvedDecisions = new List<ImportDecision>();
_rejectedDecisions = new List<ImportDecision<LocalTrack>>();
_approvedDecisions = new List<ImportDecision<LocalTrack>>();
var artist = Builder<Artist>.CreateNew()
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
@ -52,20 +52,21 @@ namespace NzbDrone.Core.Test.MediaFiles
var release = Builder<AlbumRelease>.CreateNew()
.With(e => e.AlbumId = album.Id)
.With(e => e.Monitored = true)
.Build();
album.AlbumReleases = new List<AlbumRelease> { release };
var tracks = Builder<Track>.CreateListOfSize(5)
.Build();
_rejectedDecisions.Add(new ImportDecision(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision<LocalTrack>(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision<LocalTrack>(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision<LocalTrack>(new LocalTrack(), new Rejection("Rejected!")));
foreach (var track in tracks)
{
_approvedDecisions.Add(new ImportDecision
_approvedDecisions.Add(new ImportDecision<LocalTrack>
(
new LocalTrack
{
@ -75,7 +76,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Tracks = new List<Track> { track },
Path = Path.Combine(artist.Path, "Alien Ant Farm - 01 - Pilot.mp3"),
Quality = new QualityModel(Quality.MP3_256),
ParsedTrackInfo = new ParsedTrackInfo
FileTrackInfo = new ParsedTrackInfo
{
ReleaseGroup = "DRONE"
}
@ -91,6 +92,11 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(new List<TrackFile>());
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesByAlbum(It.IsAny<int>()))
.Returns(new List<TrackFile>());
}
[Test]
@ -110,7 +116,7 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_only_import_approved()
{
var all = new List<ImportDecision>();
var all = new List<ImportDecision<LocalTrack>>();
all.AddRange(_rejectedDecisions);
all.AddRange(_approvedDecisions);
@ -123,9 +129,9 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_only_import_each_track_once()
{
var all = new List<ImportDecision>();
var all = new List<ImportDecision<LocalTrack>>();
all.AddRange(_approvedDecisions);
all.Add(new ImportDecision(_approvedDecisions.First().LocalTrack));
all.Add(new ImportDecision<LocalTrack>(_approvedDecisions.First().Item));
var result = Subject.Import(all, false);
@ -135,17 +141,17 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_move_new_downloads()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, true);
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, false),
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, false),
Times.Once());
}
[Test]
public void should_publish_TrackImportedEvent_for_new_downloads()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, true);
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<TrackImportedEvent>()), Times.Once());
@ -154,10 +160,10 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_not_move_existing_files()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, false);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, false);
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, false),
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, false),
Times.Never());
}
@ -165,21 +171,21 @@ namespace NzbDrone.Core.Test.MediaFiles
public void should_import_larger_files_first()
{
var fileDecision = _approvedDecisions.First();
fileDecision.LocalTrack.Size = 1.Gigabytes();
fileDecision.Item.Size = 1.Gigabytes();
var sampleDecision = new ImportDecision
var sampleDecision = new ImportDecision<LocalTrack>
(new LocalTrack
{
Artist = fileDecision.LocalTrack.Artist,
Album = fileDecision.LocalTrack.Album,
Tracks = new List<Track> { fileDecision.LocalTrack.Tracks.First() },
Artist = fileDecision.Item.Artist,
Album = fileDecision.Item.Album,
Tracks = new List<Track> { fileDecision.Item.Tracks.First() },
Path = @"C:\Test\Music\Alien Ant Farm\Alien Ant Farm - 01 - Pilot.mp3".AsOsAgnostic(),
Quality = new QualityModel(Quality.MP3_256),
Size = 80.Megabytes()
});
var all = new List<ImportDecision>();
var all = new List<ImportDecision<LocalTrack>>();
all.Add(fileDecision);
all.Add(sampleDecision);
@ -187,25 +193,25 @@ namespace NzbDrone.Core.Test.MediaFiles
results.Should().HaveCount(all.Count);
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported);
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported && d.ImportDecision.LocalTrack.Size == fileDecision.LocalTrack.Size);
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported && d.ImportDecision.Item.Size == fileDecision.Item.Size);
}
[Test]
public void should_copy_when_cannot_move_files_downloads()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false });
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false });
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, true), Times.Once());
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, true), Times.Once());
}
[Test]
public void should_use_override_importmode()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false }, ImportMode.Move);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false }, ImportMode.Move);
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, false), Times.Once());
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, false), Times.Once());
}
[Test]
@ -215,7 +221,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(Builder<TrackFile>.CreateListOfSize(1).BuildList());
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, false);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, false);
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Delete(It.IsAny<TrackFile>(), DeleteMediaFileReason.ManualOverride), Times.Once());

@ -5,32 +5,31 @@ using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Music;
using NzbDrone.Core.Test.Framework;
using System.Collections.Generic;
namespace NzbDrone.Core.Test.MediaFiles
{
[TestFixture]
public class MediaFileRepositoryFixture : DbTest<MediaFileRepository, TrackFile>
{
[Test]
public void get_files_by_artist()
private Artist artist;
private Album album;
[SetUp]
public void Setup()
{
var files = Builder<TrackFile>.CreateListOfSize(10)
.All()
.With(c => c.Id = 0)
.With(c => c.Quality =new QualityModel(Quality.MP3_192))
.BuildListOfNew();
Db.InsertMany(files);
Db.All<TrackFile>().Should().HaveCount(10);
var meta = Builder<ArtistMetadata>.CreateNew()
.With(a => a.Id = 0)
.Build();
Db.Insert(meta);
var artist = Builder<Artist>.CreateNew()
.With(a => a.ArtistMetadataId = 11)
artist = Builder<Artist>.CreateNew()
.With(a => a.ArtistMetadataId = meta.Id)
.With(a => a.Id = 0)
.Build();
Db.Insert(artist);
var album = Builder<Album>.CreateNew()
album = Builder<Album>.CreateNew()
.With(a => a.Id = 0)
.With(a => a.ArtistMetadataId = artist.ArtistMetadataId)
.Build();
@ -43,6 +42,15 @@ namespace NzbDrone.Core.Test.MediaFiles
.Build();
Db.Insert(release);
var files = Builder<TrackFile>.CreateListOfSize(10)
.All()
.With(c => c.Id = 0)
.With(c => c.Quality =new QualityModel(Quality.MP3_192))
.TheFirst(4)
.With(c => c.AlbumId = album.Id)
.BuildListOfNew();
Db.InsertMany(files);
var track = Builder<Track>.CreateListOfSize(10)
.TheFirst(1)
.With(a => a.TrackFileId = files[1].Id)
@ -59,16 +67,58 @@ namespace NzbDrone.Core.Test.MediaFiles
.With(a => a.AlbumReleaseId = release.Id)
.Build();
Db.InsertMany(track);
}
[Test]
public void get_files_by_artist()
{
VerifyData();
var artistFiles = Subject.GetFilesByArtist(artist.Id);
VerifyEagerLoaded(artistFiles);
artistFiles.Should().HaveCount(4);
artistFiles.Should().OnlyContain(c => c.Artist.Value.Id == artist.Id);
}
[Test]
public void get_files_by_album()
{
VerifyData();
var files = Subject.GetFilesByAlbum(album.Id);
VerifyEagerLoaded(files);
files.Should().HaveCount(4);
files.Should().OnlyContain(c => c.AlbumId == album.Id);
}
[Test]
public void get_files_by_relative_path()
{
VerifyData();
var files = Subject.GetFilesWithRelativePath(artist.Id, "RelativePath2");
VerifyEagerLoaded(files);
files.Should().HaveCount(1);
files.Should().OnlyContain(c => c.AlbumId == album.Id);
files.Should().OnlyContain(c => c.RelativePath == "RelativePath2");
}
private void VerifyData()
{
Db.All<Artist>().Should().HaveCount(1);
Db.All<Album>().Should().HaveCount(1);
Db.All<Track>().Should().HaveCount(10);
Db.All<TrackFile>().Should().HaveCount(10);
}
var artistFiles = Subject.GetFilesByArtist(artist.Id);
artistFiles.Should().HaveCount(4);
artistFiles.Should().OnlyContain(c => c.ArtistId == artist.Id);
private void VerifyEagerLoaded(List<TrackFile> files)
{
foreach (var file in files)
{
file.Album.IsLoaded.Should().BeTrue();
file.Artist.IsLoaded.Should().BeTrue();
file.Artist.Value.Metadata.IsLoaded.Should().BeTrue();
}
}
}
}

@ -1,149 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
{
[TestFixture]
public class FormatAudioChannelsFixture : TestBase
{
[Test]
public void should_subtract_one_from_AudioChannels_as_total_channels_if_LFE_in_AudioChannelPositionsText()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 6,
AudioChannelPositions = null,
AudioChannelPositionsText = "Front: L C R, Side: L R, LFE"
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_use_AudioChannels_as_total_channels_if_LFE_not_in_AudioChannelPositionsText()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = null,
AudioChannelPositionsText = "Front: L R"
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_return_0_if_schema_revision_is_less_than_3_and_other_properties_are_null()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = null,
AudioChannelPositionsText = null,
SchemaRevision = 2
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(0);
}
[Test]
public void should_use_AudioChannels_if_schema_revision_is_3_and_other_properties_are_null()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = null,
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_sum_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "2/0/0",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_sum_AudioChannelPositions_including_decimal()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "3/2/0.1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_cleanup_extraneous_text_from_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "Object Based / 3/2/2.1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_skip_empty_groups_in_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = " / 2/0/0.0",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_sum_first_series_of_numbers_from_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "3/2/2.1 / 3/2/2.1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_sum_dual_mono_representation_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "1+1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2.0m);
}
}
}

@ -1,70 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
{
[TestFixture]
public class FormatAudioCodecFixture : TestBase
{
[TestCase("AC-3", "AC3")]
[TestCase("E-AC-3", "EAC3")]
[TestCase("MPEG Audio", "MPEG Audio")]
[TestCase("DTS", "DTS")]
public void should_format_audio_format(string audioFormat, string expectedFormat)
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = audioFormat
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(expectedFormat);
}
[TestCase("MPEG Audio, A_MPEG/L2, , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")]
[TestCase("Vorbis, A_VORBIS, , Xiph.Org libVorbis I 20101101 (Schaufenugget)", "DB Super HDTV", "Vorbis")]
[TestCase("PCM, 1, , ", "DW DVDRip XviD-idTV", "PCM")] // Dubbed most likely
[TestCase("TrueHD, A_TRUEHD, , ", "", "TrueHD")]
[TestCase("WMA, 161, , ", "Droned.wmv", "WMA")]
[TestCase("WMA, 162, Pro, ", "B.N.S04E18.720p.WEB-DL", "WMA")]
public void should_format_audio_format(string audioFormatPack, string sceneName, string expectedFormat)
{
var split = audioFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = split[0],
AudioCodecID = split[1],
AudioProfile = split[2],
AudioCodecLibrary = split[3]
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(expectedFormat);
}
[Test]
public void should_return_MP3_for_MPEG_Audio_with_Layer_3_for_the_profile()
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = "MPEG Audio",
AudioProfile = "Layer 3"
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be("MP3");
}
[Test]
public void should_return_AudioFormat_by_default()
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = "Other Audio Format",
AudioCodecID = "Other Audio Codec"
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(mediaInfoModel.AudioFormat);
ExceptionVerification.ExpectedWarns(1);
}
}
}

@ -1,184 +0,0 @@
using System.IO;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Test.Common;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
{
[TestFixture]
public class UpdateMediaInfoServiceFixture : CoreTest<UpdateMediaInfoService>
{
private Artist _artist;
[SetUp]
public void Setup()
{
_artist = new Artist
{
Id = 1,
Path = @"C:\artist".AsOsAgnostic()
};
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.EnableMediaInfo)
.Returns(true);
}
private void GivenFileExists()
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.IsAny<string>()))
.Returns(true);
}
private void GivenSuccessfulScan()
{
Mocker.GetMock<IVideoFileInfoReader>()
.Setup(v => v.GetMediaInfo(It.IsAny<string>()))
.Returns(new MediaInfoModel());
}
private void GivenFailedScan(string path)
{
Mocker.GetMock<IVideoFileInfoReader>()
.Setup(v => v.GetMediaInfo(path))
.Returns((MediaInfoModel)null);
}
[Test]
public void should_skip_up_to_date_media_info()
{
var trackFiles = Builder<TrackFile>.CreateListOfSize(3)
.All()
.With(v => v.RelativePath = "media.flac")
.TheFirst(1)
.With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.CURRENT_MEDIA_INFO_SCHEMA_REVISION })
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(trackFiles);
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_artist.Path, "media.flac")), Times.Exactly(2));
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Exactly(2));
}
[Test]
public void should_skip_not_yet_date_media_info()
{
var trackFiles = Builder<TrackFile>.CreateListOfSize(3)
.All()
.With(v => v.RelativePath = "media.flac")
.TheFirst(1)
.With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.MINIMUM_MEDIA_INFO_SCHEMA_REVISION })
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(trackFiles);
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_artist.Path, "media.flac")), Times.Exactly(2));
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Exactly(2));
}
[Test]
public void should_update_outdated_media_info()
{
var trackFiles = Builder<TrackFile>.CreateListOfSize(3)
.All()
.With(v => v.RelativePath = "media.flac")
.TheFirst(1)
.With(v => v.MediaInfo = new MediaInfoModel())
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(trackFiles);
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_artist.Path, "media.flac")), Times.Exactly(3));
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Exactly(3));
}
[Test]
public void should_ignore_missing_files()
{
var trackFiles = Builder<TrackFile>.CreateListOfSize(2)
.All()
.With(v => v.RelativePath = "media.flac")
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(trackFiles);
GivenSuccessfulScan();
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo("media.flac"), Times.Never());
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Never());
}
[Test]
public void should_continue_after_failure()
{
var episodeFiles = Builder<TrackFile>.CreateListOfSize(2)
.All()
.With(v => v.RelativePath = "media.flac")
.TheFirst(1)
.With(v => v.RelativePath = "media2.flac")
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(episodeFiles);
GivenFileExists();
GivenSuccessfulScan();
GivenFailedScan(Path.Combine(_artist.Path, "media2.flac"));
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_artist.Path, "media.flac")), Times.Exactly(1));
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Exactly(1));
}
}
}

@ -1,115 +0,0 @@
using System.IO;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common.Categories;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
{
[TestFixture]
[DiskAccessTest]
public class VideoFileInfoReaderFixture : CoreTest<VideoFileInfoReader>
{
[SetUp]
public void Setup()
{
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.FileExists(It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.OpenReadStream(It.IsAny<string>()))
.Returns<string>(s => new FileStream(s, FileMode.Open, FileAccess.Read));
}
[Test]
public void get_runtime()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
Subject.GetRunTime(path).Seconds.Should().Be(10);
}
[Test]
public void get_info()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
var info = Subject.GetMediaInfo(path);
info.VideoCodec.Should().BeNull();
info.VideoFormat.Should().Be("AVC");
info.VideoCodecID.Should().Be("avc1");
info.VideoProfile.Should().Be("Baseline@L2.1");
info.VideoCodecLibrary.Should().Be("");
info.AudioFormat.Should().Be("AAC");
info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2");
info.AudioProfile.Should().Be("LC");
info.AudioCodecLibrary.Should().Be("");
info.AudioBitrate.Should().Be(128000);
info.AudioChannels.Should().Be(2);
info.AudioLanguages.Should().Be("English");
info.Height.Should().Be(320);
info.RunTime.Seconds.Should().Be(10);
info.ScanType.Should().Be("Progressive");
info.Subtitles.Should().Be("");
info.VideoBitrate.Should().Be(193329);
info.VideoFps.Should().Be(24);
info.Width.Should().Be(480);
}
[Test]
public void get_info_unicode()
{
var srcPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
var tempPath = GetTempFilePath();
Directory.CreateDirectory(tempPath);
var path = Path.Combine(tempPath, "H264_Pok\u00E9mon.mkv");
File.Copy(srcPath, path);
var info = Subject.GetMediaInfo(path);
info.VideoCodec.Should().BeNull();
info.VideoFormat.Should().Be("AVC");
info.VideoCodecID.Should().Be("avc1");
info.VideoProfile.Should().Be("Baseline@L2.1");
info.VideoCodecLibrary.Should().Be("");
info.AudioFormat.Should().Be("AAC");
info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2");
info.AudioProfile.Should().Be("LC");
info.AudioCodecLibrary.Should().Be("");
info.AudioBitrate.Should().Be(128000);
info.AudioChannels.Should().Be(2);
info.AudioLanguages.Should().Be("English");
info.Height.Should().Be(320);
info.RunTime.Seconds.Should().Be(10);
info.ScanType.Should().Be("Progressive");
info.Subtitles.Should().Be("");
info.VideoBitrate.Should().Be(193329);
info.VideoFps.Should().Be(24);
info.Width.Should().Be(480);
}
[Test]
public void should_dispose_file_after_scanning_mediainfo()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
var info = Subject.GetMediaInfo(path);
var stream = new FileStream(path, FileMode.Open, FileAccess.Write);
stream.Close();
}
}
}

@ -0,0 +1,202 @@
using NUnit.Framework;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Parser.Model;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using NzbDrone.Test.Common;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation.Aggregators;
using FluentAssertions;
using System.Text;
using System;
using System.Collections;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Aggregation.Aggregators
{
[TestFixture]
public class AggregateFilenameInfoFixture : CoreTest<AggregateFilenameInfo>
{
private LocalAlbumRelease GivenTracks(List<string> files, string root)
{
var tracks = files.Select(x => new LocalTrack {
Path = Path.Combine(root, x),
FileTrackInfo = new ParsedTrackInfo {
TrackNumbers = new [] { 0 },
}
}).ToList();
return new LocalAlbumRelease(tracks);
}
private void VerifyData(LocalTrack track, string artist, string title, int trackNum, int disc)
{
track.FileTrackInfo.ArtistTitle.Should().Be(artist);
track.FileTrackInfo.Title.Should().Be(title);
track.FileTrackInfo.TrackNumbers[0].Should().Be(trackNum);
track.FileTrackInfo.DiscNumber.Should().Be(disc);
}
[Test]
public void should_aggregate_filenames_example()
{
var release = GivenTracks(new List<string> {
"Adele - 19 - 101 - Daydreamer.mp3",
"Adele - 19 - 102 - Best for Last.mp3",
"Adele - 19 - 103 - Chasing Pavements.mp3",
"Adele - 19 - 203 - That's It, I Quit, I'm Moving On.mp3"
}, @"C:\incoming".AsOsAgnostic());
Subject.Aggregate(release, true);
VerifyData(release.LocalTracks[0], "Adele", "Daydreamer", 1, 1);
VerifyData(release.LocalTracks[1], "Adele", "Best for Last", 2, 1);
VerifyData(release.LocalTracks[2], "Adele", "Chasing Pavements", 3, 1);
VerifyData(release.LocalTracks[3], "Adele", "That's It, I Quit, I'm Moving On", 3, 2);
}
public static class TestCaseFactory
{
private static List<string[]> tokenList = new List<string[]> {
new [] {"trackNum2", "artist", "title", "tag"},
new [] {"trackNum3", "artist", "title", "tag"},
new [] {"trackNum2", "artist", "tag", "title"},
new [] {"trackNum3", "artist", "tag", "title"},
new [] {"trackNum2", "artist", "title"},
new [] {"trackNum3", "artist", "title"},
new [] {"artist", "tag", "trackNum2", "title"},
new [] {"artist", "tag", "trackNum3", "title"},
new [] {"artist", "trackNum2", "title", "tag"},
new [] {"artist", "trackNum3", "title", "tag"},
new [] {"artist", "trackNum2", "title"},
new [] {"artist", "trackNum3", "title"},
new [] {"artist", "title", "tag"},
new [] {"artist", "tag", "title"},
new [] {"artist", "title"},
new [] {"trackNum2", "title"},
new [] {"trackNum3", "title"},
new [] {"title"},
};
private static List<Tuple<string, string>> separators = new List<Tuple<string, string>> {
Tuple.Create(" - ", " "),
Tuple.Create("_", " "),
Tuple.Create("-", "_")
};
private static List<Tuple<string[], string, string>> otherCases = new List<Tuple<string[], string, string>> {
Tuple.Create(new [] {"track2", "title"}, " ", " "),
Tuple.Create(new [] {"track3", "title"}, " ", " ")
};
public static IEnumerable TestCases
{
get
{
int i = 0;
foreach (var tokens in tokenList)
{
foreach (var separator in separators)
{
i++;
yield return new TestCaseData(Tuple.Create(tokens, separator.Item1, separator.Item2))
.SetName($"should_aggregate_filenames_auto_{i}")
.SetDescription($"tokens: {string.Join(", ", tokens)}, separator: '{separator.Item1}', whitespace: '{separator.Item2}'");
}
}
// and a few other cases where all the permutations don't make sense
foreach (var item in otherCases)
{
i++;
yield return new TestCaseData(item)
.SetName($"should_aggregate_filenames_auto_{i}")
.SetDescription($"tokens: {string.Join(", ", item.Item1)}, separator: '{item.Item2}', whitespace: '{item.Item3}'");
}
}
}
}
private List<string> GivenFilenames(string[] fields, string fieldSeparator, string whitespace)
{
var outp = new List<string>();
for (int i = 1; i <= 3; i++)
{
var components = new List<string>();
foreach (var field in fields)
{
switch(field)
{
case "artist":
components.Add("artist name".Replace(" ", whitespace));
break;
case "tag":
components.Add("tag string ignore".Replace(" ", whitespace));
break;
case "title":
components.Add($"{(char)(96+i)} track title {i}".Replace(" ", whitespace));
break;
case "trackNum2":
components.Add(i.ToString("00"));
break;
case "trackNum3":
components.Add((100 + i).ToString("000"));
break;
}
}
outp.Add(string.Join(fieldSeparator, components) + ".mp3");
}
return outp;
}
private void VerifyDataAuto(List<LocalTrack> tracks, string[] tokens, string whitespace)
{
for (int i = 1; i <= tracks.Count; i++)
{
var info = tracks[i-1].FileTrackInfo;
if (tokens.Contains("artist"))
{
info.ArtistTitle.Should().Be("artist name".Replace(" ", whitespace));
}
if (tokens.Contains("title"))
{
info.Title.Should().Be($"{(char)(96+i)} track title {i}".Replace(" ", whitespace));
}
if (tokens.Contains("trackNum2") || tokens.Contains("trackNum3"))
{
info.TrackNumbers[0].Should().Be(i);
}
if (tokens.Contains("trackNum3"))
{
info.DiscNumber.Should().Be(1);
}
else
{
info.DiscNumber.Should().Be(0);
}
}
}
[Test, TestCaseSource(typeof(TestCaseFactory), "TestCases")]
public void should_aggregate_filenames_auto(Tuple<string[], string, string> testcase)
{
var files = GivenFilenames(testcase.Item1, testcase.Item2, testcase.Item3);
var release = GivenTracks(files, @"C:\incoming".AsOsAgnostic());
Subject.Aggregate(release, true);
VerifyDataAuto(release.LocalTracks, testcase.Item1, testcase.Item3);
}
}
}

@ -0,0 +1,216 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using System.Collections.Generic;
using System.Linq;
using System;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class AlbumDistanceFixture : CoreTest<IdentificationService>
{
private ArtistMetadata artist;
[SetUp]
public void Setup()
{
artist = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "artist")
.Build();
}
private List<Track> GivenTracks(int count)
{
return Builder<Track>
.CreateListOfSize(count)
.All()
.With(x => x.ArtistMetadata = artist)
.With(x => x.MediumNumber = 1)
.Build()
.ToList();
}
private LocalTrack GivenLocalTrack(Track track, AlbumRelease release)
{
var fileInfo = Builder<ParsedTrackInfo>
.CreateNew()
.With(x => x.Title = track.Title)
.With(x => x.CleanTitle = track.Title.CleanTrackTitle())
.With(x => x.AlbumTitle = release.Title)
.With(x => x.Disambiguation = release.Disambiguation)
.With(x => x.ReleaseMBId = release.ForeignReleaseId)
.With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name)
.With(x => x.TrackNumbers = new[] { track.AbsoluteTrackNumber })
.With(x => x.DiscCount = release.Media.Count)
.With(x => x.DiscNumber = track.MediumNumber)
.With(x => x.RecordingMBId = track.ForeignRecordingId)
.With(x => x.Country = IsoCountries.Find("US"))
.With(x => x.Label = release.Label.First())
.With(x => x.Year = (uint)release.Album.Value.ReleaseDate.Value.Year)
.Build();
var localTrack = Builder<LocalTrack>
.CreateNew()
.With(x => x.FileTrackInfo = fileInfo)
.Build();
return localTrack;
}
private List<LocalTrack> GivenLocalTracks(List<Track> tracks, AlbumRelease release)
{
var output = new List<LocalTrack>();
foreach (var track in tracks)
{
output.Add(GivenLocalTrack(track, release));
}
return output;
}
private AlbumRelease GivenAlbumRelease(string title, List<Track> tracks)
{
var album = Builder<Album>
.CreateNew()
.With(x => x.Title = title)
.With(x => x.ArtistMetadata = artist)
.Build();
var media = Builder<Medium>
.CreateListOfSize(tracks.Max(x => x.MediumNumber))
.Build()
.ToList();
return Builder<AlbumRelease>
.CreateNew()
.With(x => x.Tracks = tracks)
.With(x => x.Title = title)
.With(x => x.Album = album)
.With(x => x.Media = media)
.With(x => x.Country = new List<string> { "United States" })
.With(x => x.Label = new List<string> { "label" })
.Build();
}
private TrackMapping GivenMapping(List<LocalTrack> local, List<Track> remote)
{
var mapping = new TrackMapping();
var distances = local.Zip(remote, (l, r) => Tuple.Create(r, Subject.TrackDistance(l, r, Subject.GetTotalTrackNumber(r, remote))));
mapping.Mapping = local.Zip(distances, (l, r) => new { l, r }).ToDictionary(x => x.l, x => x.r);
mapping.LocalExtra = local.Except(mapping.Mapping.Keys).ToList();
mapping.MBExtra = remote.Except(mapping.Mapping.Values.Select(x => x.Item1)).ToList();
return mapping;
}
[Test]
public void test_identical_albums()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
[Test]
public void test_incomplete_album()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks.RemoveAt(1);
var mapping = GivenMapping(localTracks, tracks);
var dist = Subject.AlbumReleaseDistance(localTracks, release, mapping);
dist.NormalizedDistance().Should().NotBe(0.0);
dist.NormalizedDistance().Should().BeLessThan(0.2);
}
[Test]
public void test_global_artists_differ()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
release.Album.Value.ArtistMetadata = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "different artist")
.Build();
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().NotBe(0.0);
}
[Test]
public void test_comp_track_artists_match()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
release.Album.Value.ArtistMetadata = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "Various Artists")
.With(x => x.ForeignArtistId = "89ad4ac3-39f7-470e-963a-56509c546377")
.Build();
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
// TODO: there are a couple more VA tests in beets but we don't support VA yet anyway
[Test]
public void test_tracks_out_of_order()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks = new [] {1, 3, 2}.Select(x => localTracks[x-1]).ToList();
var mapping = GivenMapping(localTracks, tracks);
var dist = Subject.AlbumReleaseDistance(localTracks, release, mapping);
dist.NormalizedDistance().Should().NotBe(0.0);
dist.NormalizedDistance().Should().BeLessThan(0.2);
}
[Test]
public void test_two_medium_release()
{
var tracks = GivenTracks(3);
tracks[2].AbsoluteTrackNumber = 1;
tracks[2].MediumNumber = 2;
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
[Test]
public void test_absolute_track_numbering()
{
var tracks = GivenTracks(3);
tracks[2].AbsoluteTrackNumber = 1;
tracks[2].MediumNumber = 2;
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks[2].FileTrackInfo.DiscNumber = 2;
localTracks[2].FileTrackInfo.TrackNumbers = new[] { 3 };
var mapping = GivenMapping(localTracks, tracks);
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
}
}

@ -0,0 +1,167 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Test.Common;
using FluentAssertions;
using System.Collections.Generic;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class DistanceFixture : TestBase
{
[Test]
public void test_add()
{
var dist = new Distance();
dist.Add("add", 1.0);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"add", new List<double> { 1.0 }}} );
}
[Test]
public void test_equality()
{
var dist = new Distance();
dist.AddEquality("equality", "ghi", new List<string> { "abc", "def", "ghi" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"equality", new List<double> { 0.0 }}} );
dist.AddEquality("equality", "xyz", new List<string> { "abc", "def", "ghi" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"equality", new List<double> { 0.0, 1.0 }}} );
dist.AddEquality("equality", "abc", new List<string> { "abc", "def", "ghi" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"equality", new List<double> { 0.0, 1.0, 0.0 }}} );
}
[Test]
public void test_add_bool()
{
var dist = new Distance();
dist.AddBool("expr", true);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"expr", new List<double> { 1.0 }}} );
dist.AddBool("expr", false);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"expr", new List<double> { 1.0, 0.0 }}} );
}
[Test]
public void test_add_number()
{
var dist = new Distance();
dist.AddNumber("number", 1, 1);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"number", new List<double> { 0.0 }}} );
dist.AddNumber("number", 1, 2);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"number", new List<double> { 0.0, 1.0 }}} );
dist.AddNumber("number", 2, 1);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"number", new List<double> { 0.0, 1.0, 1.0 }}} );
dist.AddNumber("number", -1, 2);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"number", new List<double> { 0.0, 1.0, 1.0, 1.0, 1.0, 1.0 }}} );
}
[Test]
public void test_add_priority_value()
{
var dist = new Distance();
dist.AddPriority("priority", "abc", new List<string> { "abc" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0 }}} );
dist.AddPriority("priority", "def", new List<string> { "abc", "def" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 0.5 }}} );
dist.AddPriority("priority", "xyz", new List<string> { "abc", "def" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 0.5, 1.0 }}} );
}
[Test]
public void test_add_priority_list()
{
var dist = new Distance();
dist.AddPriority("priority", new List<string> { "abc" }, new List<string> { "abc" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0 }}} );
dist.AddPriority("priority", new List<string> { "def" }, new List<string> { "abc" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 1.0 }}} );
dist.AddPriority("priority", new List<string> { "abc", "xyz" }, new List<string> { "abc" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 1.0, 0.0 }}} );
dist.AddPriority("priority", new List<string> { "def", "xyz" }, new List<string> { "abc", "def" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 1.0, 0.0, 0.5 }}} );
}
[Test]
public void test_add_ratio()
{
var dist = new Distance();
dist.AddRatio("ratio", 25, 100);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"ratio", new List<double> { 0.25 }}} );
dist.AddRatio("ratio", 10, 5);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"ratio", new List<double> { 0.25, 1.0 }}} );
dist.AddRatio("ratio", -5, 5);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"ratio", new List<double> { 0.25, 1.0, 0.0 }}} );
dist.AddRatio("ratio", 5, 0);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"ratio", new List<double> { 0.25, 1.0, 0.0, 0.0 }}} );
}
[Test]
public void test_add_string()
{
var dist = new Distance();
dist.AddString("string", "abcd", "bcde");
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"string", new List<double> { 0.5 }}} );
}
[Test]
public void test_add_string_none()
{
var dist = new Distance();
dist.AddString("string", string.Empty, "bcd");
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"string", new List<double> { 1.0 }}} );
}
[Test]
public void test_add_string_both_none()
{
var dist = new Distance();
dist.AddString("string", string.Empty, string.Empty);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"string", new List<double> { 0.0 }}} );
}
[Test]
public void test_distance()
{
var dist = new Distance();
dist.Add("album", 0.5);
dist.Add("media_count", 0.25);
dist.Add("media_count", 0.75);
dist.NormalizedDistance().Should().Be(0.5);
}
[Test]
public void test_max_distance()
{
var dist = new Distance();
dist.Add("album", 0.5);
dist.Add("media_count", 0.0);
dist.Add("media_count", 0.0);
dist.MaxDistance().Should().Be(5.0);
}
[Test]
public void test_raw_distance()
{
var dist = new Distance();
dist.Add("album", 0.5);
dist.Add("media_count", 0.25);
dist.Add("media_count", 0.5);
dist.RawDistance().Should().Be(2.25);
}
}
}

@ -0,0 +1,155 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using System.Collections.Generic;
using System.Linq;
using System;
using NzbDrone.Core.Parser;
using NzbDrone.Common.Serializer;
using Moq;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class GetCandidatesFixture : CoreTest<IdentificationService>
{
private ArtistMetadata artist;
[SetUp]
public void Setup()
{
artist = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "artist")
.Build();
}
private List<Track> GivenTracks(int count)
{
return Builder<Track>
.CreateListOfSize(count)
.All()
.With(x => x.ArtistMetadata = artist)
.Build()
.ToList();
}
private ParsedTrackInfo GivenParsedTrackInfo(Track track, AlbumRelease release)
{
return Builder<ParsedTrackInfo>
.CreateNew()
.With(x => x.Title = track.Title)
.With(x => x.AlbumTitle = release.Title)
.With(x => x.Disambiguation = release.Disambiguation)
.With(x => x.ReleaseMBId = release.ForeignReleaseId)
.With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name)
.With(x => x.TrackNumbers = new[] { track.AbsoluteTrackNumber })
.With(x => x.RecordingMBId = track.ForeignRecordingId)
.With(x => x.Country = IsoCountries.Find("US"))
.With(x => x.Label = release.Label.First())
.With(x => x.Year = (uint)release.Album.Value.ReleaseDate.Value.Year)
.Build();
}
private List<LocalTrack> GivenLocalTracks(List<Track> tracks, AlbumRelease release)
{
var output = Builder<LocalTrack>
.CreateListOfSize(tracks.Count)
.Build()
.ToList();
for (int i = 0; i < tracks.Count; i++)
{
output[i].FileTrackInfo = GivenParsedTrackInfo(tracks[i], release);
}
return output;
}
private AlbumRelease GivenAlbumRelease(string title, List<Track> tracks)
{
var album = Builder<Album>
.CreateNew()
.With(x => x.Title = title)
.With(x => x.ArtistMetadata = artist)
.Build();
var media = Builder<Medium>
.CreateListOfSize(1)
.Build()
.ToList();
return Builder<AlbumRelease>
.CreateNew()
.With(x => x.Tracks = tracks)
.With(x => x.Title = title)
.With(x => x.Album = album)
.With(x => x.Media = media)
.With(x => x.Country = new List<string>())
.With(x => x.Label = new List<string> { "label" })
.With(x => x.ForeignReleaseId = null)
.Build();
}
private LocalAlbumRelease GivenLocalAlbumRelease()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
return new LocalAlbumRelease(localTracks);
}
[Test]
public void get_candidates_by_fingerprint_should_not_fail_if_fingerprint_lookup_returned_null()
{
Mocker.GetMock<IFingerprintingService>()
.Setup(x => x.Lookup(It.IsAny<List<LocalTrack>>(), It.IsAny<double>()))
.Callback((List<LocalTrack> x, double thres) => {
foreach(var track in x) {
track.AcoustIdResults = null;
}
});
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesByRecordingIds(It.IsAny<List<string>>()))
.Returns(new List<AlbumRelease>());
var local = GivenLocalAlbumRelease();
Subject.GetCandidatesFromFingerprint(local).ShouldBeEquivalentTo(new List<AlbumRelease>());
}
[Test]
public void get_candidates_should_only_return_specified_release_if_set()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var localAlbumRelease = new LocalAlbumRelease(localTracks);
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, release).ShouldBeEquivalentTo(new List<AlbumRelease> { release });
}
[Test]
public void get_candidates_should_use_consensus_release_id()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
release.ForeignReleaseId = "xxx";
var localTracks = GivenLocalTracks(tracks, release);
var localAlbumRelease = new LocalAlbumRelease(localTracks);
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesByForeignReleaseId(new List<string>{ "xxx" }))
.Returns(new List<AlbumRelease> { release });
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, null).ShouldBeEquivalentTo(new List<AlbumRelease> { release });
}
}
}

@ -0,0 +1,140 @@
using System.IO;
using System.Linq;
using System.Collections;
using FluentAssertions;
using FluentValidation.Results;
using Moq;
using Newtonsoft.Json;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.MetadataSource.SkyHook;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class IdentificationServiceFixture : DbTest
{
private ArtistService _artistService;
private AddArtistService _addArtistService;
private RefreshArtistService _refreshArtistService;
private IdentificationService Subject;
[SetUp]
public void SetUp()
{
UseRealHttp();
// Resolve all the parts we need
Mocker.SetConstant<IArtistRepository>(Mocker.Resolve<ArtistRepository>());
Mocker.SetConstant<IArtistMetadataRepository>(Mocker.Resolve<ArtistMetadataRepository>());
Mocker.SetConstant<IAlbumRepository>(Mocker.Resolve<AlbumRepository>());
Mocker.SetConstant<IReleaseRepository>(Mocker.Resolve<ReleaseRepository>());
Mocker.SetConstant<ITrackRepository>(Mocker.Resolve<TrackRepository>());
Mocker.GetMock<IMetadataProfileService>().Setup(x => x.Exists(It.IsAny<int>())).Returns(true);
_artistService = Mocker.Resolve<ArtistService>();
Mocker.SetConstant<IArtistService>(_artistService);
Mocker.SetConstant<IAlbumService>(Mocker.Resolve<AlbumService>());
Mocker.SetConstant<IReleaseService>(Mocker.Resolve<ReleaseService>());
Mocker.SetConstant<ITrackService>(Mocker.Resolve<TrackService>());
Mocker.SetConstant<IConfigService>(Mocker.Resolve<IConfigService>());
Mocker.SetConstant<IProvideArtistInfo>(Mocker.Resolve<SkyHookProxy>());
Mocker.SetConstant<IProvideAlbumInfo>(Mocker.Resolve<SkyHookProxy>());
_addArtistService = Mocker.Resolve<AddArtistService>();
Mocker.SetConstant<IRefreshTrackService>(Mocker.Resolve<RefreshTrackService>());
Mocker.SetConstant<IAddAlbumService>(Mocker.Resolve<AddAlbumService>());
_refreshArtistService = Mocker.Resolve<RefreshArtistService>();
Mocker.GetMock<IAddArtistValidator>().Setup(x => x.Validate(It.IsAny<Artist>())).Returns(new ValidationResult());
Mocker.SetConstant<ITrackGroupingService>(Mocker.Resolve<TrackGroupingService>());
Subject = Mocker.Resolve<IdentificationService>();
}
private void GivenMetadataProfile(MetadataProfile profile)
{
Mocker.GetMock<IMetadataProfileService>().Setup(x => x.Get(It.IsAny<int>())).Returns(profile);
}
private Artist GivenArtist(string foreignArtistId)
{
var artist = _addArtistService.AddArtist(new Artist {
Metadata = new ArtistMetadata {
ForeignArtistId = foreignArtistId
},
Path = @"c:\test".AsOsAgnostic(),
MetadataProfileId = 1
});
var command = new RefreshArtistCommand{
ArtistId = artist.Id,
Trigger = CommandTrigger.Unspecified
};
_refreshArtistService.Execute(command);
return _artistService.FindById(foreignArtistId);
}
public static class IdTestCaseFactory
{
// for some reason using Directory.GetFiles causes nUnit to error
private static string[] files = {
"FilesWithMBIds.json",
"PreferMissingToBadMatch.json",
"InconsistentTyposInAlbum.json",
"SucceedWhenManyAlbumsHaveSameTitle.json",
"PenalizeUnknownMedia.json"
};
public static IEnumerable TestCases
{
get
{
foreach (var file in files)
{
yield return new TestCaseData(file).SetName($"should_match_tracks_{file.Replace(".json", "")}");
}
}
}
}
// these are slow to run so only do so manually
[Explicit]
[Test, TestCaseSource(typeof(IdTestCaseFactory), "TestCases")]
public void should_match_tracks(string file)
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Identification", file);
var testcase = JsonConvert.DeserializeObject<IdTestCase>(File.ReadAllText(path));
GivenMetadataProfile(testcase.MetadataProfile);
var artist = GivenArtist(testcase.Artist);
var tracks = testcase.Tracks.Select(x => new LocalTrack {
Path = x.Path.AsOsAgnostic(),
FileTrackInfo = x.FileTrackInfo
}).ToList();
var result = Subject.Identify(tracks, artist, null, null, testcase.NewDownload, testcase.SingleRelease);
result.Should().HaveCount(testcase.ExpectedMusicBrainzReleaseIds.Count);
result.Select(x => x.AlbumRelease.ForeignReleaseId).ShouldBeEquivalentTo(testcase.ExpectedMusicBrainzReleaseIds);
}
}
}

@ -0,0 +1,184 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Test.Common;
using FluentAssertions;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class MunkresFixture : TestBase
{
// 2d arrays don't play nicely with attributes
public void RunTest(double[,] costMatrix, double expectedCost)
{
var m = new Munkres(costMatrix);
m.Run();
m.Cost.Should().Be(expectedCost);
}
[Test]
public void MunkresSquareTest1()
{
var C = new double[,] {
{ 1, 2, 3 },
{ 2, 4, 6 },
{ 3, 6, 9 }
};
RunTest(C, 10);
}
[Test]
public void MunkresSquareTest2()
{
var C = new double[,] {
{ 400, 150, 400 },
{ 400, 450, 600 },
{ 300, 225, 300 }
};
RunTest(C, 850);
}
[Test]
public void MunkresSquareTest3()
{
var C = new double[,] {
{ 10, 10, 8 },
{ 9, 8, 1 },
{ 9, 7, 4 }
};
RunTest(C, 18);
}
[Test]
public void MunkresSquareTest4()
{
var C = new double[,] {
{ 5, 9, 1 },
{ 10, 3, 2 },
{ 8, 7, 4 }
};
RunTest(C, 12);
}
[Test]
public void MunkresSquareTest5()
{
var C = new double[,] {
{12, 26, 17, 0, 0},
{49, 43, 36, 10, 5},
{97, 9, 66, 34, 0},
{52, 42, 19, 36, 0},
{15, 93, 55, 80, 0}
};
RunTest(C, 48);
}
[Test]
public void Munkres5x5Test()
{
var C = new double[,] {
{12, 9, 27, 10, 23},
{7, 13, 13, 30, 19},
{25, 18, 26, 11, 26},
{9, 28, 26, 23, 13},
{16, 16, 24, 6, 9}
};
RunTest(C, 51);
}
[Test]
public void Munkres10x10Test()
{
var C = new double[,] {
{37, 34, 29, 26, 19, 8, 9, 23, 19, 29},
{9, 28, 20, 8, 18, 20, 14, 33, 23, 14},
{15, 26, 12, 28, 6, 17, 9, 13, 21, 7},
{2, 8, 38, 36, 39, 5, 36, 2, 38, 27},
{30, 3, 33, 16, 21, 39, 7, 23, 28, 36},
{7, 5, 19, 22, 36, 36, 24, 19, 30, 2},
{34, 20, 13, 36, 12, 33, 9, 10, 23, 5},
{7, 37, 22, 39, 33, 39, 10, 3, 13, 26},
{21, 25, 23, 39, 31, 37, 32, 33, 38, 1},
{17, 34, 40, 10, 29, 37, 40, 3, 25, 3}
};
RunTest(C, 66);
}
[Test]
public void Munkres20x20Test()
{
var C = new double[,] {
{5, 4, 3, 9, 8, 9, 3, 5, 6, 9, 4, 10, 3, 5, 6, 6, 1, 8, 10, 2},
{10, 9, 9, 2, 8, 3, 9, 9, 10, 1, 7, 10, 8, 4, 2, 1, 4, 8, 4, 8},
{10, 4, 4, 3, 1, 3, 5, 10, 6, 8, 6, 8, 4, 10, 7, 2, 4, 5, 1, 8},
{2, 1, 4, 2, 3, 9, 3, 4, 7, 3, 4, 1, 3, 2, 9, 8, 6, 5, 7, 8},
{3, 4, 4, 1, 4, 10, 1, 2, 6, 4, 5, 10, 2, 2, 3, 9, 10, 9, 9, 10},
{1, 10, 1, 8, 1, 3, 1, 7, 1, 1, 2, 1, 2, 6, 3, 3, 4, 4, 8, 6},
{1, 8, 7, 10, 10, 3, 4, 6, 1, 6, 6, 4, 9, 6, 9, 6, 4, 5, 4, 7},
{8, 10, 3, 9, 4, 9, 3, 3, 4, 6, 4, 2, 6, 7, 7, 4, 4, 3, 4, 7},
{1, 3, 8, 2, 6, 9, 2, 7, 4, 8, 10, 8, 10, 5, 1, 3, 10, 10, 2, 9},
{2, 4, 1, 9, 2, 9, 7, 8, 2, 1, 4, 10, 5, 2, 7, 6, 5, 7, 2, 6},
{4, 5, 1, 4, 2, 3, 3, 4, 1, 8, 8, 2, 6, 9, 5, 9, 6, 3, 9, 3},
{3, 1, 1, 8, 6, 8, 8, 7, 9, 3, 2, 1, 8, 2, 4, 7, 3, 1, 2, 4},
{5, 9, 8, 6, 10, 4, 10, 3, 4, 10, 10, 10, 1, 7, 8, 8, 7, 7, 8, 8},
{1, 4, 6, 1, 6, 1, 2, 10, 5, 10, 2, 6, 2, 4, 5, 5, 3, 5, 1, 5},
{5, 6, 9, 10, 6, 6, 10, 6, 4, 1, 5, 3, 9, 5, 2, 10, 9, 9, 5, 1},
{10, 9, 4, 6, 9, 5, 3, 7, 10, 1, 6, 8, 1, 1, 10, 9, 5, 7, 7, 5},
{2, 6, 6, 6, 6, 2, 9, 4, 7, 5, 3, 2, 10, 3, 4, 5, 10, 9, 1, 7},
{5, 2, 4, 9, 8, 4, 8, 2, 4, 1, 3, 7, 6, 8, 1, 6, 8, 8, 10, 10},
{9, 6, 3, 1, 8, 5, 7, 8, 7, 2, 1, 8, 2, 8, 3, 7, 4, 8, 7, 7},
{8, 4, 4, 9, 7, 10, 6, 2, 1, 5, 8, 5, 1, 1, 1, 9, 1, 3, 5, 3}
};
RunTest(C, 22);
}
[Test]
public void MunkresRectangularTest1()
{
var C = new double[,] {
{ 400, 150, 400, 1 },
{ 400, 450, 600, 2 },
{ 300, 225, 300, 3 }
};
RunTest(C, 452);
}
[Test]
public void MunkresRectangularTest2()
{
var C = new double[,] {
{ 10, 10, 8, 11 },
{ 9, 8, 1, 1 },
{ 9, 7, 4, 10 }
};
RunTest(C, 15);
}
[Test]
public void MunkresRectangularTest3()
{
var C = new double[,] {
{34, 26, 17, 12},
{43, 43, 36, 10},
{97, 47, 66, 34},
{52, 42, 19, 36},
{15, 93, 55, 80}
};
RunTest(C, 70);
}
}
}

@ -0,0 +1,99 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class TrackDistanceFixture : CoreTest<IdentificationService>
{
private Track GivenTrack(string title)
{
var artist = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "artist")
.Build();
var mbTrack = Builder<Track>
.CreateNew()
.With(x => x.Title = title)
.With(x => x.ArtistMetadata = artist)
.Build();
return mbTrack;
}
private LocalTrack GivenLocalTrack(Track track)
{
var fileInfo = Builder<ParsedTrackInfo>
.CreateNew()
.With(x => x.Title = track.Title)
.With(x => x.CleanTitle = track.Title.CleanTrackTitle())
.With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name)
.With(x => x.TrackNumbers = new[] { 1 })
.With(x => x.RecordingMBId = track.ForeignRecordingId)
.Build();
var localTrack = Builder<LocalTrack>
.CreateNew()
.With(x => x.FileTrackInfo = fileInfo)
.Build();
return localTrack;
}
[Test]
public void test_identical_tracks()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
}
[Test]
public void test_feat_removed_from_localtrack()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.Title = "one (feat. two)";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
}
[Test]
public void test_different_title()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.CleanTitle = "foo";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0);
}
[Test]
public void test_different_artist()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.ArtistTitle = "foo";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0);
}
[Test]
public void test_various_artists_tolerated()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.ArtistTitle = "Various Artists";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
}
}
}

@ -0,0 +1,369 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Test.Common;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using FizzWare.NBuilder.PropertyNaming;
using System.Reflection;
using System.Text;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
// we need to use random strings to test the va (so we don't just get artist1, artist2 etc which are too similar)
// but the standard random value namer would give paths that are too long on windows
public class RandomValueNamerShortStrings : RandomValuePropertyNamer
{
private readonly IRandomGenerator generator;
private static readonly List<char> allowedChars;
public RandomValueNamerShortStrings(BuilderSettings settings) : base(settings)
{
generator = new RandomGenerator();
}
static RandomValueNamerShortStrings()
{
allowedChars = new List<char>();
for (char c = 'a'; c < 'z'; c++)
{
allowedChars.Add(c);
}
for (char c = 'A'; c < 'Z'; c++)
{
allowedChars.Add(c);
}
for (char c = '0'; c < '9'; c++)
{
allowedChars.Add(c);
}
}
protected override string GetString(MemberInfo memberInfo)
{
int length = generator.Next(0, 100);
char[] chars = new char[length];
for (int i = 0; i < length; i++)
{
int index = generator.Next(0, allowedChars.Count - 1);
chars[i] = allowedChars[index];
}
byte[] bytes = Encoding.UTF8.GetBytes(chars);
return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
}
[TestFixture]
public class TrackGroupingServiceFixture : CoreTest<TrackGroupingService>
{
private List<LocalTrack> GivenTracks(string root, string artist, string album, int count)
{
var fileInfos = Builder<ParsedTrackInfo>
.CreateListOfSize(count)
.All()
.With(f => f.ArtistTitle = artist)
.With(f => f.AlbumTitle = album)
.With(f => f.AlbumMBId = null)
.With(f => f.ReleaseMBId = null)
.Build();
var tracks = fileInfos.Select(x => Builder<LocalTrack>
.CreateNew()
.With(y => y.FileTrackInfo = x)
.With(y => y.Path = Path.Combine(root, x.Title))
.Build()).ToList();
return tracks;
}
private List<LocalTrack> GivenTracksWithNoTags(string root, int count)
{
var outp = new List<LocalTrack>();
for (int i = 0; i < count; i++)
{
var track = Builder<LocalTrack>
.CreateNew()
.With(y => y.FileTrackInfo = new ParsedTrackInfo())
.With(y => y.Path = Path.Combine(root, $"{i}.mp3"))
.Build();
outp.Add(track);
}
return outp;
}
private List<LocalTrack> GivenVaTracks(string root, string album, int count)
{
var settings = new BuilderSettings();
settings.SetPropertyNamerFor<ParsedTrackInfo>(new RandomValueNamerShortStrings(settings));
var builder = new Builder(settings);
var fileInfos = builder
.CreateListOfSize<ParsedTrackInfo>(count)
.All()
.With(f => f.AlbumTitle = "album")
.With(f => f.AlbumMBId = null)
.With(f => f.ReleaseMBId = null)
.Build();
var tracks = fileInfos.Select(x => Builder<LocalTrack>
.CreateNew()
.With(y => y.FileTrackInfo = x)
.With(y => y.Path = Path.Combine(@"C:\music\incoming".AsOsAgnostic(), x.Title))
.Build()).ToList();
return tracks;
}
[TestCase(1)]
[TestCase(2)]
[TestCase(10)]
public void single_artist_is_not_various_artists(int count)
{
var tracks = GivenTracks(@"C:\music\incoming".AsOsAgnostic(), "artist", "album", count);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
}
[Test]
public void all_different_artists_is_various_artists()
{
var tracks = GivenVaTracks(@"C:\music\incoming".AsOsAgnostic(), "album", 10);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(true);
}
[Test]
public void two_artists_is_not_various_artists()
{
var dir = @"C:\music\incoming".AsOsAgnostic();
var tracks = GivenTracks(dir, "artist1", "album", 10);
tracks.AddRange(GivenTracks(dir, "artist2", "album", 10));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
}
[Test]
public void mostly_different_artists_is_various_artists()
{
var dir = @"C:\music\incoming".AsOsAgnostic();
var tracks = GivenVaTracks(dir, "album", 10);
tracks.AddRange(GivenTracks(dir, "single_artist", "album", 2));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(true);
}
[TestCase("")]
[TestCase("Various Artists")]
[TestCase("Various")]
[TestCase("VA")]
[TestCase("Unknown")]
public void va_artist_title_is_various_artists(string artist)
{
var tracks = GivenTracks(@"C:\music\incoming".AsOsAgnostic(), artist, "album", 10);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(true);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(10)]
public void should_group_single_artist_album(int count)
{
var tracks = GivenTracks(@"C:\music\incoming".AsOsAgnostic(), "artist", "album", count);
var output = Subject.GroupTracks(tracks);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(count);
}
[TestCase("cd")]
[TestCase("disc")]
[TestCase("disk")]
public void should_group_multi_disc_release(string mediaName)
{
var tracks = GivenTracks($"C:\\music\\incoming\\artist - album\\{mediaName} 1".AsOsAgnostic(),
"artist", "album", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\artist - album\\{mediaName} 2".AsOsAgnostic(),
"artist", "album", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(15);
}
[Test]
public void should_not_group_two_different_albums_by_same_artist()
{
var tracks = GivenTracks($"C:\\music\\incoming\\artist - album1".AsOsAgnostic(),
"artist", "album1", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\artist - album2".AsOsAgnostic(),
"artist", "album2", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(10);
output[1].LocalTracks.Count.Should().Be(5);
}
[Test]
public void should_group_albums_with_typos()
{
var tracks = GivenTracks($"C:\\music\\incoming\\artist - album".AsOsAgnostic(),
"artist", "Rastaman Vibration (Remastered)", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\artist - album".AsOsAgnostic(),
"artist", "Rastaman Vibration (Remastered", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(15);
}
[Test]
public void should_not_group_two_different_tracks_in_same_directory()
{
var tracks = GivenTracks($"C:\\music\\incoming".AsOsAgnostic(),
"artist", "album1", 1);
tracks.AddRange(GivenTracks($"C:\\music\\incoming".AsOsAgnostic(),
"artist", "album2", 1));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(1);
output[1].LocalTracks.Count.Should().Be(1);
}
[Test]
public void should_separate_two_albums_in_same_directory()
{
var tracks = GivenTracks($"C:\\music\\incoming\\artist discog".AsOsAgnostic(),
"artist", "album1", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\artist disog".AsOsAgnostic(),
"artist", "album2", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(10);
output[1].LocalTracks.Count.Should().Be(5);
}
[Test]
public void should_separate_many_albums_in_same_directory()
{
var tracks = new List<LocalTrack>();
for (int i = 0; i < 100; i++)
{
tracks.AddRange(GivenTracks($"C:\\music".AsOsAgnostic(),
"artist" + i, "album" + i, 10));
}
// don't test various artists here because it's designed to only work if there's a common album
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(100);
output.Select(x => x.LocalTracks.Count).Distinct().ShouldBeEquivalentTo(new List<int> { 10 });
}
[Test]
public void should_separate_two_albums_by_different_artists_in_same_directory()
{
var tracks = GivenTracks($"C:\\music\\incoming".AsOsAgnostic(),
"artist1", "album1", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming".AsOsAgnostic(),
"artist2", "album2", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(10);
output[1].LocalTracks.Count.Should().Be(5);
}
[Test]
public void should_group_va_release()
{
var tracks = GivenVaTracks(@"C:\music\incoming".AsOsAgnostic(), "album", 10);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(true);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(10);
}
[Test]
public void should_not_group_two_albums_by_different_artists_with_same_title()
{
var tracks = GivenTracks($"C:\\music\\incoming\\album".AsOsAgnostic(),
"artist1", "album", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\album".AsOsAgnostic(),
"artist2", "album", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(10);
output[1].LocalTracks.Count.Should().Be(5);
}
[Test]
public void should_not_fail_if_all_tags_null()
{
var tracks = GivenTracksWithNoTags($"C:\\music\\incoming\\album".AsOsAgnostic(), 10);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(10);
}
[Test]
public void should_not_fail_if_some_tags_null()
{
var tracks = GivenTracks($"C:\\music\\incoming\\album".AsOsAgnostic(),
"artist1", "album", 10);
tracks.AddRange(GivenTracksWithNoTags($"C:\\music\\incoming\\album".AsOsAgnostic(), 2));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(12);
}
}
}

@ -0,0 +1,187 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using System.Collections.Generic;
using System.Linq;
using System;
using NzbDrone.Core.Parser;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class TrackMappingFixture : CoreTest<IdentificationService>
{
private ArtistMetadata artist;
[SetUp]
public void Setup()
{
artist = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "artist")
.Build();
}
private List<Track> GivenTracks(int count)
{
return Builder<Track>
.CreateListOfSize(count)
.All()
.With(x => x.ArtistMetadata = artist)
.Build()
.ToList();
}
private ParsedTrackInfo GivenParsedTrackInfo(Track track, AlbumRelease release)
{
return Builder<ParsedTrackInfo>
.CreateNew()
.With(x => x.Title = track.Title)
.With(x => x.CleanTitle = track.Title.CleanTrackTitle())
.With(x => x.AlbumTitle = release.Title)
.With(x => x.Disambiguation = release.Disambiguation)
.With(x => x.ReleaseMBId = release.ForeignReleaseId)
.With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name)
.With(x => x.TrackNumbers = new[] { track.AbsoluteTrackNumber })
.With(x => x.RecordingMBId = track.ForeignRecordingId)
.With(x => x.Country = IsoCountries.Find("US"))
.With(x => x.Label = release.Label.First())
.With(x => x.Year = (uint)release.Album.Value.ReleaseDate.Value.Year)
.Build();
}
private List<LocalTrack> GivenLocalTracks(List<Track> tracks, AlbumRelease release)
{
var output = Builder<LocalTrack>
.CreateListOfSize(tracks.Count)
.Build()
.ToList();
for (int i = 0; i < tracks.Count; i++)
{
output[i].FileTrackInfo = GivenParsedTrackInfo(tracks[i], release);
}
return output;
}
private AlbumRelease GivenAlbumRelease(string title, List<Track> tracks)
{
var album = Builder<Album>
.CreateNew()
.With(x => x.Title = title)
.With(x => x.ArtistMetadata = artist)
.Build();
var media = Builder<Medium>
.CreateListOfSize(1)
.Build()
.ToList();
return Builder<AlbumRelease>
.CreateNew()
.With(x => x.Tracks = tracks)
.With(x => x.Title = title)
.With(x => x.Album = album)
.With(x => x.Media = media)
.With(x => x.Country = new List<string>())
.With(x => x.Label = new List<string> { "label" })
.Build();
}
[Test]
public void test_reorder_when_track_numbers_incorrect()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks[2].FileTrackInfo.TrackNumbers = new [] { 2 };
localTracks[1].FileTrackInfo.TrackNumbers = new [] { 3 };
localTracks = new [] {0, 2, 1}.Select(x => localTracks[x]).ToList();
var result = Subject.MapReleaseTracks(localTracks, tracks);
result.Mapping
.ToDictionary(x => x.Key, y => y.Value.Item1)
.ShouldBeEquivalentTo(new Dictionary<LocalTrack, Track> {
{localTracks[0], tracks[0]},
{localTracks[1], tracks[2]},
{localTracks[2], tracks[1]},
});
result.LocalExtra.Should().BeEmpty();
result.MBExtra.Should().BeEmpty();
}
[Test]
public void test_order_works_with_invalid_track_numbers()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
foreach (var track in localTracks)
{
track.FileTrackInfo.TrackNumbers = new[] { 1 };
}
var result = Subject.MapReleaseTracks(localTracks, tracks);
result.Mapping
.ToDictionary(x => x.Key, y => y.Value.Item1)
.ShouldBeEquivalentTo(new Dictionary<LocalTrack, Track> {
{localTracks[0], tracks[0]},
{localTracks[1], tracks[1]},
{localTracks[2], tracks[2]},
});
result.LocalExtra.Should().BeEmpty();
result.MBExtra.Should().BeEmpty();
}
[Test]
public void test_order_works_with_missing_tracks()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks.RemoveAt(1);
var result = Subject.MapReleaseTracks(localTracks, tracks);
result.Mapping
.ToDictionary(x => x.Key, y => y.Value.Item1)
.ShouldBeEquivalentTo(new Dictionary<LocalTrack, Track> {
{localTracks[0], tracks[0]},
{localTracks[1], tracks[2]}
});
result.LocalExtra.Should().BeEmpty();
result.MBExtra.ShouldBeEquivalentTo(new List<Track> { tracks[1] });
}
[Test]
public void test_order_works_with_extra_tracks()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
tracks.RemoveAt(1);
var result = Subject.MapReleaseTracks(localTracks, tracks);
result.Mapping
.ToDictionary(x => x.Key, y => y.Value.Item1)
.ShouldBeEquivalentTo(new Dictionary<LocalTrack, Track> {
{localTracks[0], tracks[0]},
{localTracks[2], tracks[1]}
});
result.LocalExtra.ShouldBeEquivalentTo(new List<LocalTrack> { localTracks[1] });
result.MBExtra.Should().BeEmpty();
}
}
}

@ -6,16 +6,18 @@ using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.TrackImport;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Test.Common;
using FizzWare.NBuilder;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Profiles.Languages;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport
{
@ -25,27 +27,54 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
private List<string> _audioFiles;
private LocalTrack _localTrack;
private Artist _artist;
private AlbumRelease _albumRelease;
private QualityModel _quality;
private Mock<IImportDecisionEngineSpecification> _pass1;
private Mock<IImportDecisionEngineSpecification> _pass2;
private Mock<IImportDecisionEngineSpecification> _pass3;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass1;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass2;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass3;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumfail1;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumfail2;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumfail3;
private Mock<IImportDecisionEngineSpecification> _fail1;
private Mock<IImportDecisionEngineSpecification> _fail2;
private Mock<IImportDecisionEngineSpecification> _fail3;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _pass1;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _pass2;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _pass3;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _fail1;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _fail2;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _fail3;
[SetUp]
public void Setup()
{
_pass1 = new Mock<IImportDecisionEngineSpecification>();
_pass2 = new Mock<IImportDecisionEngineSpecification>();
_pass3 = new Mock<IImportDecisionEngineSpecification>();
_albumpass1 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumpass2 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumpass3 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_fail1 = new Mock<IImportDecisionEngineSpecification>();
_fail2 = new Mock<IImportDecisionEngineSpecification>();
_fail3 = new Mock<IImportDecisionEngineSpecification>();
_albumfail1 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumfail2 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumfail3 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_pass1 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_pass2 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_pass3 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_fail1 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_fail2 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_fail3 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_albumpass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Accept());
_albumpass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Accept());
_albumpass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Accept());
_albumfail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Reject("_albumfail1"));
_albumfail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Reject("_albumfail2"));
_albumfail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Reject("_albumfail3"));
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>())).Returns(Decision.Accept());
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>())).Returns(Decision.Accept());
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>())).Returns(Decision.Accept());
@ -59,25 +88,33 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
.With(e => e.LanguageProfile = new LanguageProfile { Languages = Languages.LanguageFixture.GetDefaultLanguages() })
.Build();
_albumRelease = Builder<AlbumRelease>.CreateNew()
.Build();
_quality = new QualityModel(Quality.MP3_256);
_localTrack = new LocalTrack
{
{
Artist = _artist,
Quality = _quality,
Language = Language.Spanish,
Tracks = new List<Track> { new Track() },
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.Spanish.XviD-OSiTV.avi"
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi"
};
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()))
.Returns(_localTrack);
GivenVideoFiles(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) => {
var ret = new LocalAlbumRelease(tracks);
ret.AlbumRelease = _albumRelease;
return new List<LocalAlbumRelease> { ret };
});
GivenVideoFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.Spanish.XviD-OSiTV.avi".AsOsAgnostic() });
GivenSpecifications(_albumpass1);
}
private void GivenSpecifications(params Mock<IImportDecisionEngineSpecification>[] mocks)
private void GivenSpecifications<T>(params Mock<IImportDecisionEngineSpecification<T>>[] mocks)
{
Mocker.SetConstant(mocks.Select(c => c.Object));
}
@ -91,177 +128,142 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
.Returns(_audioFiles);
}
[Test]
public void should_call_all_specifications()
private void GivenAugmentationSuccess()
{
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null);
_fail1.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
Mocker.GetMock<IAugmentingService>()
.Setup(s => s.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
.Callback<LocalTrack, bool>((localTrack, otherFiles) =>
{
localTrack.Tracks = _localTrack.Tracks;
});
}
[Test]
public void should_return_rejected_if_single_specs_fail()
public void should_call_all_album_specifications()
{
GivenSpecifications(_fail1);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().Approved.Should().BeFalse();
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
GivenAugmentationSuccess();
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
_albumfail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumfail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumfail3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumpass1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumpass2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumpass3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
}
[Test]
public void should_return_rejected_if_one_of_specs_fail()
public void should_call_all_track_specifications_if_album_accepted()
{
GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_return_pass_if_all_specs_pass()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
result.Single().Approved.Should().BeTrue();
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
}
[Test]
public void should_have_same_number_of_rejections_as_specs_that_failed()
public void should_call_no_track_specifications_if_album_rejected()
{
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
GivenAugmentationSuccess();
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().Rejections.Should().HaveCount(3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_fail3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_pass3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
}
[Test]
public void should_not_blowup_the_process_due_to_failed_parse()
public void should_return_rejected_if_only_album_spec_fails()
{
GivenSpecifications(_albumfail1);
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()))
.Throws<TestException>();
_audioFiles = new List<string>
{
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
GivenVideoFiles(_audioFiles);
Subject.GetImportDecisions(_audioFiles, _artist);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()), Times.Exactly(_audioFiles.Count));
ExceptionVerification.ExpectedErrors(3);
}
[Test]
public void should_use_file_quality_if_folder_quality_is_null()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_audioFiles.Single(), null, 0);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
var result = Subject.GetImportDecisions(_audioFiles, _artist);
result.Single().LocalTrack.Quality.Should().Be(expectedQuality);
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_use_file_language_if_folder_language_is_null()
public void should_return_rejected_if_only_track_spec_fails()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedLanguage = LanguageParser.ParseLanguage(_audioFiles.Single());
GivenSpecifications(_albumpass1);
GivenSpecifications(_fail1);
var result = Subject.GetImportDecisions(_audioFiles, _artist);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().LocalTrack.Language.Should().Be(expectedLanguage);
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_use_file_quality_if_file_quality_was_determined_by_name()
[Test]
public void should_return_rejected_if_one_album_spec_fails()
{
GivenSpecifications(_albumpass1, _albumfail1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_audioFiles.Single(), null, 0);
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo{Quality = new QualityModel(Quality.MP3_256) });
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().LocalTrack.Quality.Should().Be(expectedQuality);
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_use_folder_quality_when_file_quality_was_determined_by_the_extension()
public void should_return_rejected_if_one_track_spec_fails()
{
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localTrack.Path = _audioFiles.Single();
_localTrack.Quality.QualitySource = QualitySource.Extension;
_localTrack.Quality.Quality = Quality.MP3_256;
var expectedQuality = new QualityModel(Quality.MP3_256);
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Quality = expectedQuality });
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().LocalTrack.Quality.Should().Be(expectedQuality);
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_use_folder_quality_when_greater_than_file_quality()
public void should_return_approved_if_all_specs_pass()
{
GivenAugmentationSuccess();
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localTrack.Path = _audioFiles.Single();
_localTrack.Quality.Quality = Quality.MP3_256;
var expectedQuality = new QualityModel(Quality.MP3_256);
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Quality = expectedQuality });
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().LocalTrack.Quality.Should().Be(expectedQuality);
result.Single().Approved.Should().BeTrue();
}
[Test]
public void should_use_folder_language_when_greater_than_file_language()
public void should_have_same_number_of_rejections_as_specs_that_failed()
{
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.Spanish.mkv".AsOsAgnostic() });
_localTrack.Path = _audioFiles.Single();
_localTrack.Quality.Quality = Quality.MP3_320;
_localTrack.Language = Language.Spanish;
var expectedLanguage = Language.French;
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Language = expectedLanguage, Quality = new QualityModel(Quality.MP3_192) });
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
result.Single().LocalTrack.Language.Should().Be(expectedLanguage);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().Rejections.Should().HaveCount(3);
}
[Test]
public void should_not_throw_if_episodes_are_not_found()
[Test]
public void should_not_blowup_the_process_due_to_failed_augment()
{
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()))
.Returns(new LocalTrack() { Path = "test" });
Mocker.GetMock<IAugmentingService>()
.Setup(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
.Throws<TestException>();
_audioFiles = new List<string>
{
@ -272,129 +274,71 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenVideoFiles(_audioFiles);
var decisions = Subject.GetImportDecisions(_audioFiles, _artist);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()), Times.Exactly(_audioFiles.Count));
decisions.Should().HaveCount(3);
decisions.First().Rejections.Should().NotBeEmpty();
}
[Test]
public void should_not_use_folder_for_full_season()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Artist.Title.S01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Artist.Title.S01\S01E02.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Artist.Title.S01\S01E03.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseMusicTitle("Artist.Title.S01");
Subject.GetImportDecisions(_audioFiles, _artist, folderInfo);
Subject.GetImportDecisions(_audioFiles, _artist);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), null), Times.Exactly(3));
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.Is<ParsedTrackInfo>(p => p != null)), Times.Never());
ExceptionVerification.ExpectedErrors(3);
}
[Test]
public void should_not_use_folder_when_it_contains_more_than_one_valid_video_file()
public void should_not_throw_if_release_not_identified()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Artist.Title.S01E01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Artist.Title.S01E01\1x01.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseMusicTitle("Artist.Title.S01E01");
Subject.GetImportDecisions(_audioFiles, _artist, folderInfo);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), null), Times.Exactly(2));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.Is<ParsedTrackInfo>(p => p != null)), Times.Never());
}
[Test]
public void should_use_folder_when_only_one_video_file()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Artist.Title.S01E01\S01E01.mkv".AsOsAgnostic()
};
_audioFiles = new List<string>
{
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
GivenVideoFiles(_audioFiles);
var folderInfo = Parser.Parser.ParseMusicTitle("Artist.Title.S01E01");
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) => {
return new List<LocalAlbumRelease> { new LocalAlbumRelease(tracks) };
});
Subject.GetImportDecisions(_audioFiles, _artist, folderInfo);
var decisions = Subject.GetImportDecisions(_audioFiles, _artist);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()), Times.Exactly(1));
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), null), Times.Never());
decisions.Should().HaveCount(3);
decisions.First().Rejections.Should().NotBeEmpty();
}
[Test]
public void should_not_use_folder_name_if_file_name_is_scene_name()
public void should_not_throw_if_tracks_are_not_found()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Artist.Title.S01E01.720p.HDTV-LOL\Artist.Title.S01E01.720p.HDTV-LOL.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseMusicTitle("Artist.Title.S01E01.720p.HDTV-LOL");
Subject.GetImportDecisions(_audioFiles, _artist, folderInfo);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), null), Times.Exactly(1));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.Is<ParsedTrackInfo>(p => p != null)), Times.Never());
}
[Test]
public void should_not_use_folder_quality_when_it_is_unknown()
{
GivenSpecifications(_pass1, _pass2, _pass3);
_artist.Profile = new Profile
{
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_256, Quality.Unknown)
};
_audioFiles = new List<string>
{
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
GivenVideoFiles(_audioFiles);
var folderQuality = new QualityModel(Quality.Unknown);
var decisions = Subject.GetImportDecisions(_audioFiles, _artist);
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Quality = folderQuality});
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
result.Single().LocalTrack.Quality.Should().Be(_quality);
decisions.Should().HaveCount(3);
decisions.First().Rejections.Should().NotBeEmpty();
}
[Test]
public void should_return_a_decision_when_exception_is_caught()
{
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()))
Mocker.GetMock<IAugmentingService>()
.Setup(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
.Throws<TestException>();
_audioFiles = new List<string>

@ -37,6 +37,7 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistServiceTests
[TestCase("The Black Eyde Peas", "The Black Eyed Peas")]
[TestCase("Black Eyed Peas", "The Black Eyed Peas")]
[TestCase("The Black eys", "The Black Keys")]
[TestCase("Black Keys", "The Black Keys")]
public void should_find_artist_in_db_by_name_inexact(string name, string expected)
{
var artist = Subject.FindByNameInexact(name);
@ -45,6 +46,20 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistServiceTests
artist.Name.Should().Be(expected);
}
[Test]
public void should_find_artist_when_the_is_omitted_from_start()
{
_artists = new List<Artist>();
_artists.Add(CreateArtist("Black Keys"));
_artists.Add(CreateArtist("The Black Eyed Peas"));
Mocker.GetMock<IArtistRepository>()
.Setup(s => s.All())
.Returns(_artists);
Subject.FindByNameInexact("The Black Keys").Should().NotBeNull();
}
[TestCase("The Black Peas")]
public void should_not_find_artist_in_db_by_ambiguous_name(string name)
{

@ -1,178 +0,0 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Core.Test.Framework;
using System.Collections.Generic;
using Moq;
namespace NzbDrone.Core.Test.MusicTests.TitleMatchingTests
{
[TestFixture]
public class TitleMatchingFixture : CoreTest<TrackService>
{
private List<Track> _tracks;
[SetUp]
public void Setup()
{
var trackNames = new List<string> {
"Courage",
"Movies",
"Flesh and Bone",
"Whisper",
"Summer",
"Sticks and Stones",
"Attitude",
"Stranded",
"Wish",
"Calico",
"(Happy) Death Day",
"Smooth Criminal",
"Universe / Orange Appeal",
"Christian's Inferno"
};
_tracks = new List<Track>();
for (int i = 0; i < trackNames.Count; i++) {
_tracks.Add(new Track
{
Title = trackNames[i],
ForeignTrackId = (i+1).ToString(),
AbsoluteTrackNumber = i+1,
MediumNumber = 1
});
}
Mocker.GetMock<ITrackRepository>()
.Setup(s => s.GetTracksByMedium(It.IsAny<int>(), It.IsAny<int>()))
.Returns(_tracks);
Mocker.GetMock<ITrackRepository>()
.Setup(s => s.Find(1234, 4321, It.IsAny<int>(), It.IsAny<int>()))
.Returns((int artistid, int albumid, int medium, int track) => _tracks.Where(t => t.AbsoluteTrackNumber == track && t.MediumNumber == medium).Single());
}
private void GivenSecondDisc()
{
var trackNames = new List<string> {
"Courage",
"another entry",
"random name"
};
for (int i = 0; i < trackNames.Count; i++) {
_tracks.Add(new Track
{
Title = trackNames[i],
ForeignTrackId = (100+i+1).ToString(),
AbsoluteTrackNumber = i+1,
MediumNumber = 2
});
}
}
[Test]
public void should_find_track_in_db_by_tracktitle_longer_then_releasetitle()
{
var track = Subject.FindTrackByTitle(1234, 4321, 1, 1, "Courage with some bla");
track.Should().NotBeNull();
track.Title.Should().Be(Subject.FindTrack(1234, 4321, 1, 1).Title);
}
[Test]
public void should_find_track_in_db_by_tracktitle_shorter_then_releasetitle()
{
var track = Subject.FindTrackByTitle(1234, 4321, 1, 3, "and Bone");
track.Should().NotBeNull();
track.Title.Should().Be(Subject.FindTrack(1234, 4321, 1, 3).Title);
}
[Test]
public void should_not_find_track_in_db_by_wrong_title()
{
var track = Subject.FindTrackByTitle(1234, 4321, 1, 1, "Not a track");
track.Should().BeNull();
}
[TestCase("another entry", 2, 2)]
[TestCase("random name", 2, 3)]
public void should_find_track_on_second_disc_when_disc_tag_missing(string title, int discNumber, int trackNumber)
{
GivenSecondDisc();
var track = Subject.FindTrackByTitle(1234, 4321, 0, trackNumber, title);
var expected = Subject.FindTrack(1234, 4321, discNumber, trackNumber);
track.Should().NotBeNull();
expected.Should().NotBeNull();
track.Title.Should().Be(expected.Title);
}
[Test]
public void should_return_null_if_tracks_with_same_name_and_number_on_different_discs()
{
GivenSecondDisc();
var track = Subject.FindTrackByTitle(1234, 4321, 0, 1, "Courage");
track.Should().BeNull();
}
[TestCase("Fesh and Bone", 3)]
[TestCase("Atitude", 7)]
[TestCase("Smoth cRimnal", 12)]
[TestCase("Sticks and Stones (live)", 6)]
[TestCase("Sticks and Stones (live) - there's a lot of rubbish here", 6)]
[TestCase("Smoth cRimnal feat. someone I don't care about", 12)]
[TestCase("Christians Inferno", 14)]
[TestCase("xxxyyy some random prefix Christians Infurno", 14)]
public void should_find_track_in_db_by_inexact_title(string title, int trackNumber)
{
var track = Subject.FindTrackByTitleInexact(1234, 4321, 1, trackNumber, title);
var expected = Subject.FindTrack(1234, 4321, 1, trackNumber);
track.Should().NotBeNull();
expected.Should().NotBeNull();
track.Title.Should().Be(expected.Title);
}
[TestCase("Fesh and Bone", 1)]
[TestCase("Atitude", 1)]
[TestCase("Smoth cRimnal", 1)]
[TestCase("Sticks and Stones (live)", 1)]
[TestCase("Christians Inferno", 1)]
public void should_not_find_track_in_db_by_inexact_title_with_wrong_tracknumber(string title, int trackNumber)
{
var track = Subject.FindTrackByTitleInexact(1234, 4321, 1, trackNumber, title);
track.Should().BeNull();
}
[TestCase("Movis", 1, 2)]
[TestCase("anoth entry", 2, 2)]
[TestCase("random.name", 2, 3)]
public void should_find_track_in_db_by_inexact_title_when_disc_tag_missing(string title, int discNumber, int trackNumber)
{
GivenSecondDisc();
var track = Subject.FindTrackByTitleInexact(1234, 4321, 0, trackNumber, title);
var expected = Subject.FindTrack(1234, 4321, discNumber, trackNumber);
track.Should().NotBeNull();
expected.Should().NotBeNull();
track.Title.Should().Be(expected.Title);
}
[TestCase("A random title", 1)]
[TestCase("Stones and Sticks", 6)]
public void should_not_find_track_in_db_by_different_inexact_title(string title, int trackId)
{
var track = Subject.FindTrackByTitleInexact(1234, 4321, 1, trackId, title);
track.Should().BeNull();
}
}
}

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -285,8 +285,15 @@
<Compile Include="MediaFiles\DownloadedAlbumsCommandServiceFixture.cs" />
<Compile Include="MediaFiles\DownloadedTracksImportServiceFixture.cs" />
<Compile Include="MediaFiles\MediaFileDeletionService\DeleteTrackFileFixture.cs" />
<Compile Include="MediaFiles\MediaInfo\MediaInfoFormatterTests\FormatAudioChannelsFixture.cs" />
<Compile Include="MediaFiles\MediaInfo\MediaInfoFormatterTests\FormatAudioCodecFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Aggregation\AggregateFilenameInfoFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\MunkresFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\DistanceFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\TrackGroupingServiceFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\TrackDistanceFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\AlbumDistanceFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\TrackMappingFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\GetCandidatesFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\IdentificationServiceFixture.cs" />
<Compile Include="MediaFiles\TrackFileMovingServiceTests\MoveTrackFileFixture.cs" />
<Compile Include="MediaFiles\TrackImport\ImportDecisionMakerFixture.cs" />
<Compile Include="MediaFiles\TrackImport\Specifications\FreeSpaceSpecificationFixture.cs" />
@ -310,13 +317,12 @@
<Compile Include="MusicTests\ArtistServiceTests\FindByNameInexactFixture.cs" />
<Compile Include="MusicTests\RefreshAlbumServiceFixture.cs" />
<Compile Include="MusicTests\ShouldRefreshAlbumFixture.cs" />
<Compile Include="MusicTests\TitleMatchingTests\TitleMatchingFixture.cs" />
<Compile Include="NotificationTests\NotificationBaseFixture.cs" />
<Compile Include="NotificationTests\SynologyIndexerFixture.cs" />
<Compile Include="OrganizerTests\FileNameBuilderTests\CleanTitleFixture.cs" />
<Compile Include="OrganizerTests\FileNameBuilderTests\TitleTheFixture.cs" />
<Compile Include="ParserTests\MusicParserFixture.cs" />
<Compile Include="ParserTests\ParsingServiceTests\GetLocalTrackFixture.cs" />
<Compile Include="ParserTests\FingerprintingServiceFixture.cs" />
<Compile Include="Profiles\Delay\DelayProfileServiceFixture.cs" />
<Compile Include="Profiles\Metadata\MetadataProfileServiceFixture.cs" />
<Compile Include="Profiles\Metadata\MetadataProfileRepositoryFixture.cs" />
@ -327,8 +333,6 @@
<Compile Include="OrganizerTests\CleanFixture.cs" />
<Compile Include="MediaFiles\MediaFileServiceTests\FilterFixture.cs" />
<Compile Include="MediaFiles\MediaFileTableCleanupServiceFixture.cs" />
<Compile Include="MediaFiles\MediaInfo\UpdateMediaInfoServiceFixture.cs" />
<Compile Include="MediaFiles\MediaInfo\VideoFileInfoReaderFixture.cs" />
<Compile Include="MediaFiles\RenameTrackFileServiceFixture.cs" />
<Compile Include="MediaFiles\UpgradeMediaFileServiceFixture.cs" />
<Compile Include="Messaging\Commands\CommandEqualityComparerFixture.cs" />
@ -498,6 +502,12 @@
<Content Include="Files\Media\H264_sample.mp4">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Media\nin.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Media\nin.flac">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Nzbget\JsonError.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@ -594,8 +604,14 @@
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
</Target> -->
<Target Name="AfterBuild">
<ItemGroup>
<IdentificationTestCases Include="Files\Identification\*.json" />
</ItemGroup>
<Copy
SourceFiles="@(IdentificationTestCases)"
DestinationFolder="$(OutputPath)\Files\Identification\"
SkipUnchangedFiles="true"/>
</Target>
-->
</Project>
</Project>

@ -400,38 +400,6 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be("Linkin.Park.06");
}
[Test]
public void should_format_mediainfo_properly()
{
_namingConfig.StandardTrackFormat = "{Artist.Name}.{track:00}.{Track.Title}.{MEDIAINFO.FULL}";
_trackFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel()
{
AudioFormat = "FLAC",
AudioLanguages = "English/Spanish",
Subtitles = "English/Spanish/Italian"
};
Subject.BuildTrackFileName(new List<Track> { _track1 }, _artist, _album, _trackFile)
.Should().Be("Linkin.Park.06.City.Sushi.FLAC[EN+ES].[EN+ES+IT]");
}
[Test]
public void should_exclude_english_in_mediainfo_audio_language()
{
_namingConfig.StandardTrackFormat = "{Artist.Name}.{track:00}.{Track.Title}.{MEDIAINFO.FULL}";
_trackFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel
{
AudioFormat = "FLAC",
AudioLanguages = "English",
Subtitles = "English/Spanish/Italian"
};
Subject.BuildTrackFileName(new List<Track> { _track1 }, _artist, _album, _trackFile)
.Should().Be("Linkin.Park.06.City.Sushi.FLAC.[EN+ES+IT]");
}
[Test]
public void should_remove_duplicate_non_word_characters()
{

File diff suppressed because one or more lines are too long

@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.ParserTests
public void should_parse_quality_from_extension(string title)
{
Parser.Parser.ParseAlbumTitle(title).Quality.Quality.Should().NotBe(Quality.Unknown);
Parser.Parser.ParseAlbumTitle(title).Quality.QualitySource.Should().Be(QualitySource.Extension);
Parser.Parser.ParseAlbumTitle(title).Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.Extension);
}
[TestCase("VA - The Best 101 Love Ballads (2017) MP3 [192 kbps]", "VA", "The Best 101 Love Ballads")]

@ -1,127 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
[TestFixture]
public class GetLocalTrackFixture : CoreTest<ParsingService>
{
private Artist _fakeArtist;
private Album _fakeAlbum;
private Track _fakeTrack;
private ParsedTrackInfo _parsedTrackInfo;
[SetUp]
public void Setup()
{
_fakeArtist = Builder<Artist>
.CreateNew()
.Build();
_fakeAlbum = Builder<Album>
.CreateNew()
.With(e => e.ArtistId = _fakeArtist.Id)
.With(e => e.AlbumReleases = new List<AlbumRelease>
{
new AlbumRelease
{
ForeignReleaseId = "5ecd552b-e54b-4c37-b62c-9d6234834bad",
Monitored = true
}
})
.Build();
_fakeTrack = Builder<Track>
.CreateNew()
.With(e => e.Artist = _fakeArtist)
.With(e => e.AlbumId = _fakeAlbum.Id)
.With(e => e.Album = null)
.Build();
_parsedTrackInfo = Builder<ParsedTrackInfo>
.CreateNew()
.With(e => e.AlbumTitle = _fakeAlbum.Title)
.With(e => e.Title = _fakeTrack.Title)
.With(e => e.ArtistTitle = _fakeArtist.Name)
.Build();
Mocker.GetMock<IAlbumService>()
.Setup(s => s.FindByTitle(_fakeArtist.Id,_fakeAlbum.Title))
.Returns(_fakeAlbum);
Mocker.GetMock<IAlbumService>()
.Setup(s => s.FindAlbumByRelease(_fakeAlbum.AlbumReleases.Value.First().ForeignReleaseId))
.Returns(_fakeAlbum);
Mocker.GetMock<ITrackService>()
.Setup(s => s.FindTrackByTitle(_fakeArtist.Id, _fakeAlbum.Id, It.IsAny<int>(), It.IsAny<int>(), _fakeTrack.Title))
.Returns(_fakeTrack);
}
private void HasAlbumTitleNoReleaseId()
{
_parsedTrackInfo.AlbumTitle = _fakeAlbum.Title;
_parsedTrackInfo.ReleaseMBId = "";
}
private void HasReleaseMbIdNoTitle()
{
_parsedTrackInfo.AlbumTitle = "";
_parsedTrackInfo.ReleaseMBId = _fakeAlbum.AlbumReleases.Value.First().ForeignReleaseId;
}
private void HasNoReleaseIdOrTitle()
{
_parsedTrackInfo.AlbumTitle = "";
_parsedTrackInfo.ReleaseMBId = "";
}
[Test]
public void should_find_album_with_title_no_MBID()
{
HasAlbumTitleNoReleaseId();
var localTrack = Subject.GetLocalTrack("somfile.mp3", _fakeArtist, _parsedTrackInfo);
localTrack.Artist.Id.Should().Be(_fakeArtist.Id);
localTrack.Album.Id.Should().Be(_fakeAlbum.Id);
localTrack.Tracks.First().Id.Should().Be(_fakeTrack.Id);
}
[Test]
public void should_find_album_with_release_MBID_no_title()
{
HasReleaseMbIdNoTitle();
var localTrack = Subject.GetLocalTrack("somfile.mp3", _fakeArtist, _parsedTrackInfo);
localTrack.Artist.Id.Should().Be(_fakeArtist.Id);
localTrack.Album.Id.Should().Be(_fakeAlbum.Id);
localTrack.Tracks.First().Id.Should().Be(_fakeTrack.Id);
}
[Test]
public void should_not_find_album_with_no_release_MBID_no_title()
{
HasNoReleaseIdOrTitle();
var localTrack = Subject.GetLocalTrack("somfile.mp3", _fakeArtist, _parsedTrackInfo);
ExceptionVerification.ExpectedWarns(1);
localTrack.Should().BeNull();
}
}
}

@ -262,7 +262,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Little Mix - Salute [Deluxe Edition] [2013] [M4A-256]-V3nom [GLT")]
public void should_parse_quality_from_name(string title)
{
QualityParser.ParseQuality(title, null, 0).QualitySource.Should().Be(QualitySource.Name);
QualityParser.ParseQuality(title, null, 0).QualityDetectionSource.Should().Be(QualityDetectionSource.Name);
}
[TestCase("01. Kanye West - Ultralight Beam.mp3")]
@ -273,7 +273,7 @@ namespace NzbDrone.Core.Test.ParserTests
//[TestCase("01. Kanye West - Ultralight Beam.wav")]
public void should_parse_quality_from_extension(string title)
{
QualityParser.ParseQuality(title, null, 0).QualitySource.Should().Be(QualitySource.Extension);
QualityParser.ParseQuality(title, null, 0).QualityDetectionSource.Should().Be(QualityDetectionSource.Extension);
}
private void ParseAndVerifyQuality(string name, string desc, int bitrate, Quality quality, int sampleSize = 0)

@ -1,5 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Cache;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.ArtistStats
{
@ -9,25 +14,33 @@ namespace NzbDrone.Core.ArtistStats
ArtistStatistics ArtistStatistics(int artistId);
}
public class ArtistStatisticsService : IArtistStatisticsService
public class ArtistStatisticsService : IArtistStatisticsService,
IHandle<ArtistUpdatedEvent>,
IHandle<ArtistDeletedEvent>,
IHandle<AlbumImportedEvent>,
IHandle<AlbumEditedEvent>,
IHandle<TrackFileDeletedEvent>
{
private readonly IArtistStatisticsRepository _artistStatisticsRepository;
private readonly ICached<List<AlbumStatistics>> _cache;
public ArtistStatisticsService(IArtistStatisticsRepository artistStatisticsRepository)
public ArtistStatisticsService(IArtistStatisticsRepository artistStatisticsRepository,
ICacheManager cacheManager)
{
_artistStatisticsRepository = artistStatisticsRepository;
_cache = cacheManager.GetCache<List<AlbumStatistics>>(GetType());
}
public List<ArtistStatistics> ArtistStatistics()
{
var albumStatistics = _artistStatisticsRepository.ArtistStatistics();
var albumStatistics = _cache.Get("AllArtists", () => _artistStatisticsRepository.ArtistStatistics());
return albumStatistics.GroupBy(s => s.ArtistId).Select(s => MapArtistStatistics(s.ToList())).ToList();
}
public ArtistStatistics ArtistStatistics(int artistId)
{
var stats = _artistStatisticsRepository.ArtistStatistics(artistId);
var stats = _cache.Get(artistId.ToString(), () => _artistStatisticsRepository.ArtistStatistics(artistId));
if (stats == null || stats.Count == 0) return new ArtistStatistics();
@ -49,5 +62,40 @@ namespace NzbDrone.Core.ArtistStats
return artistStatistics;
}
[EventHandleOrder(EventHandleOrder.First)]
public void Handle(ArtistUpdatedEvent message)
{
_cache.Remove("AllArtists");
_cache.Remove(message.Artist.Id.ToString());
}
[EventHandleOrder(EventHandleOrder.First)]
public void Handle(ArtistDeletedEvent message)
{
_cache.Remove("AllArtists");
_cache.Remove(message.Artist.Id.ToString());
}
[EventHandleOrder(EventHandleOrder.First)]
public void Handle(AlbumImportedEvent message)
{
_cache.Remove("AllArtists");
_cache.Remove(message.Artist.Id.ToString());
}
[EventHandleOrder(EventHandleOrder.First)]
public void Handle(AlbumEditedEvent message)
{
_cache.Remove("AllArtists");
_cache.Remove(message.Album.ArtistId.ToString());
}
[EventHandleOrder(EventHandleOrder.First)]
public void Handle(TrackFileDeletedEvent message)
{
_cache.Remove("AllArtists");
_cache.Remove(message.TrackFile.Artist.Value.Id.ToString());
}
}
}

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Configuration
{
public enum AllowFingerprinting
{
Never,
NewFiles,
AllFiles
}
}

@ -195,13 +195,6 @@ namespace NzbDrone.Core.Configuration
set { SetValue("CopyUsingHardlinks", value); }
}
public bool EnableMediaInfo
{
get { return GetValueBoolean("EnableMediaInfo", true); }
set { SetValue("EnableMediaInfo", value); }
}
public bool ImportExtraFiles
{
get { return GetValueBoolean("ImportExtraFiles", false); }
@ -223,6 +216,13 @@ namespace NzbDrone.Core.Configuration
set { SetValue("RescanAfterRefresh", value); }
}
public AllowFingerprinting AllowFingerprinting
{
get { return GetValueEnum("AllowFingerprinting", AllowFingerprinting.NewFiles); }
set { SetValue("AllowFingerprinting", value); }
}
public bool SetPermissionsLinux
{
get { return GetValueBoolean("SetPermissionsLinux", false); }

@ -30,10 +30,10 @@ namespace NzbDrone.Core.Configuration
FileDateType FileDate { get; set; }
bool SkipFreeSpaceCheckWhenImporting { get; set; }
bool CopyUsingHardlinks { get; set; }
bool EnableMediaInfo { get; set; }
bool ImportExtraFiles { get; set; }
string ExtraFileExtensions { get; set; }
RescanAfterRefreshType RescanAfterRefresh { get; set; }
AllowFingerprinting AllowFingerprinting { get; set; }
//Permissions (Media Management)
bool SetPermissionsLinux { get; set; }

@ -31,6 +31,7 @@ namespace NzbDrone.Core.Datastore
bool HasItems();
void DeleteMany(IEnumerable<int> ids);
void SetFields(TModel model, params Expression<Func<TModel, object>>[] properties);
void SetFields(IEnumerable<TModel> models, params Expression<Func<TModel, object>>[] properties);
TModel Single();
PagingSpec<TModel> GetPaged(PagingSpec<TModel> pagingSpec);
}
@ -244,6 +245,30 @@ namespace NzbDrone.Core.Datastore
ModelUpdated(model);
}
public void SetFields(IEnumerable<TModel> models, params Expression<Func<TModel, object>>[] properties)
{
using (var unitOfWork = new UnitOfWork(() => DataMapper))
{
unitOfWork.BeginTransaction(IsolationLevel.ReadCommitted);
foreach (var model in models)
{
if (model.Id == 0)
{
throw new InvalidOperationException("Can't update model with ID 0");
}
unitOfWork.DB.Update<TModel>()
.Where(c => c.Id == model.Id)
.ColumnsIncluding(properties)
.Entity(model)
.Execute();
}
unitOfWork.Commit();
}
}
public virtual PagingSpec<TModel> GetPaged(PagingSpec<TModel> pagingSpec)
{
pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList();

@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(024)]
public class NewMediaInfoFormat : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Update.Table("TrackFiles").Set(new { MediaInfo = "" }).AllRows();
}
}
}

@ -40,6 +40,7 @@ using NzbDrone.Core.Extras.Lyrics;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Music;
using NzbDrone.Core.Languages;
using Marr.Data.QGen;
namespace NzbDrone.Core.Datastore
{
@ -107,8 +108,10 @@ namespace NzbDrone.Core.Datastore
.For(rg => rg.AlbumReleases)
.LazyLoad(condition: rg => rg.Id > 0, query: (db, rg) => db.Query<AlbumRelease>().Where(r => r.AlbumId == rg.Id).ToList())
.For(rg => rg.Artist)
.LazyLoad(condition: rg => rg.ArtistMetadataId > 0, query: (db, rg) => db.Query<Artist>().Where(a => a.ArtistMetadataId == rg.ArtistMetadataId).SingleOrDefault());
.LazyLoad(condition: rg => rg.ArtistMetadataId > 0,
query: (db, rg) => db.Query<Artist>()
.Join<Artist, ArtistMetadata>(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id)
.Where(a => a.ArtistMetadataId == rg.ArtistMetadataId).SingleOrDefault());
Mapper.Entity<AlbumRelease>().RegisterModel("AlbumReleases")
.Relationship()
@ -119,40 +122,41 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<Track>().RegisterModel("Tracks")
.Ignore(t => t.HasFile)
.Ignore(t => t.AlbumId)
.Ignore(t => t.ArtistId)
.Ignore(t => t.Album)
.Relationship()
.HasOne(track => track.AlbumRelease, track => track.AlbumReleaseId)
.HasOne(track => track.ArtistMetadata, track => track.ArtistMetadataId)
.HasOne(track => track.TrackFile, track => track.TrackFileId)
.For(track => track.TrackFile)
.LazyLoad(condition: track => track.TrackFileId > 0,
query: (db, track) => db.Query<TrackFile>()
.Join<TrackFile, Track>(JoinType.Inner, t => t.Tracks, (t, x) => t.Id == x.TrackFileId)
.Join<TrackFile, Album>(JoinType.Inner, t => t.Album, (t, a) => t.AlbumId == a.Id)
.Join<TrackFile, Artist>(JoinType.Inner, t => t.Artist, (t, a) => t.Album.Value.ArtistMetadataId == a.ArtistMetadataId)
.Join<Artist, ArtistMetadata>(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id)
.Where(t => t.Id == track.TrackFileId)
.SingleOrDefault())
.For(t => t.Artist)
.LazyLoad(condition: t => t.AlbumReleaseId > 0, query: (db, t) => db.Query<Artist>().QueryText(string.Format(
"/* LazyLoading Artist for Track */\n" +
"SELECT Artists.* " +
"FROM Artists " +
"JOIN Albums ON Albums.ArtistMetadataId = Artists.ArtistMetadataId " +
"JOIN AlbumReleases ON AlbumReleases.AlbumId = Albums.Id " +
"WHERE AlbumReleases.Id = {0}",
t.AlbumReleaseId)).SingleOrDefault());
.LazyLoad(condition: t => t.AlbumReleaseId > 0, query: (db, t) => db.Query<Artist>()
.Join<Artist, ArtistMetadata>(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id)
.Join<Artist, Album>(JoinType.Inner, a => a.Albums, (l, r) => l.ArtistMetadataId == r.ArtistMetadataId)
.Join<Album, AlbumRelease>(JoinType.Inner, a => a.AlbumReleases, (l, r) => l.Id == r.AlbumId)
.Where<AlbumRelease>(r => r.Id == t.AlbumReleaseId)
.SingleOrDefault());
Mapper.Entity<TrackFile>().RegisterModel("TrackFiles")
.Ignore(f => f.Path)
.Relationship()
.HasOne(f => f.Album, f => f.AlbumId)
.For("Tracks")
.LazyLoad(condition: parent => parent.Id > 0,
query: (db, parent) => db.Query<Track>().Where(c => c.TrackFileId == parent.Id).ToList())
.For(f => f.Tracks)
.LazyLoad(condition: f => f.Id > 0, query: (db, f) => db.Query<Track>()
.Where(x => x.TrackFileId == f.Id)
.ToList())
.For(t => t.Artist)
.LazyLoad(condition: f => f.Id > 0, query: (db, f) => db.Query<Artist>().QueryText(string.Format(
"/* LazyLoading Artist for TrackFile */\n" +
"SELECT Artists.* " +
"FROM Artists " +
"JOIN Albums ON Albums.ArtistMetadataId = Artists.ArtistMetadataId " +
"JOIN AlbumReleases ON AlbumReleases.AlbumId = Albums.Id " +
"JOIN Tracks ON Tracks.AlbumReleaseId = AlbumReleases.Id " +
"JOIN TrackFiles ON TrackFiles.Id = Tracks.TrackFileId " +
"WHERE TrackFiles.Id = {0}",
f.Id)).SingleOrDefault());
.LazyLoad(condition: f => f.Id > 0, query: (db, f) => db.Query<Artist>()
.Join<Artist, ArtistMetadata>(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id)
.Join<Artist, Album>(JoinType.Inner, a => a.Albums, (l, r) => l.ArtistMetadataId == r.ArtistMetadataId)
.Where<Album>(r => r.Id == f.AlbumId)
.SingleOrDefault());
Mapper.Entity<QualityDefinition>().RegisterModel("QualityDefinitions")
.Ignore(d => d.GroupName)

@ -4,6 +4,8 @@ using NLog;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Music;
using NzbDrone.Common.Cache;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
@ -11,13 +13,21 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{
private readonly UpgradableSpecification _upgradableSpecification;
private readonly IMediaFileService _mediaFileService;
private readonly ITrackService _trackService;
private readonly Logger _logger;
public CutoffSpecification(UpgradableSpecification upgradableSpecification, Logger logger, IMediaFileService mediaFileService)
private readonly ICached<bool> _missingFilesCache;
public CutoffSpecification(UpgradableSpecification upgradableSpecification,
Logger logger,
ICacheManager cacheManager,
IMediaFileService mediaFileService,
ITrackService trackService)
{
_upgradableSpecification = upgradableSpecification;
_logger = logger;
_mediaFileService = mediaFileService;
_trackService = trackService;
_missingFilesCache = cacheManager.GetCache<bool>(GetType());
}
public SpecificationPriority Priority => SpecificationPriority.Default;
@ -30,9 +40,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
foreach (var album in subject.Albums)
{
var tracksMissing = _missingFilesCache.Get(album.Id.ToString(), () => _trackService.TracksWithoutFiles(album.Id).Any(),
TimeSpan.FromSeconds(30));
var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
if (trackFiles.Any())
if (!tracksMissing && trackFiles.Any())
{
var lowestQuality = trackFiles.Select(c => c.Quality).OrderBy(c => c.Quality.Id).First();

@ -4,20 +4,30 @@ using NLog;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Music;
using NzbDrone.Common.Cache;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class UpgradeDiskSpecification : IDecisionEngineSpecification
{
private readonly IMediaFileService _mediaFileService;
private readonly ITrackService _trackService;
private readonly UpgradableSpecification _upgradableSpecification;
private readonly Logger _logger;
public UpgradeDiskSpecification(UpgradableSpecification qualityUpgradableSpecification, IMediaFileService mediaFileService, Logger logger)
private readonly ICached<bool> _missingFilesCache;
public UpgradeDiskSpecification(UpgradableSpecification qualityUpgradableSpecification,
IMediaFileService mediaFileService,
ITrackService trackService,
ICacheManager cacheManager,
Logger logger)
{
_upgradableSpecification = qualityUpgradableSpecification;
_mediaFileService = mediaFileService;
_trackService = trackService;
_logger = logger;
_missingFilesCache = cacheManager.GetCache<bool>(GetType());
}
public SpecificationPriority Priority => SpecificationPriority.Default;
@ -28,9 +38,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
foreach (var album in subject.Albums)
{
var tracksMissing = _missingFilesCache.Get(album.Id.ToString(), () => _trackService.TracksWithoutFiles(album.Id).Any(),
TimeSpan.FromSeconds(30));
var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
if (trackFiles.Any())
if (!tracksMissing && trackFiles.Any())
{
var lowestQuality = trackFiles.Select(c => c.Quality).OrderBy(c => c.Quality.Id).First();

@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
@ -13,6 +12,7 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Music;
using NzbDrone.Core.MediaFiles.TrackImport;
using NzbDrone.Core.MediaFiles.Events;
namespace NzbDrone.Core.Download
{
@ -107,11 +107,14 @@ namespace NzbDrone.Core.Download
if (importResults.Empty())
{
trackedDownload.State = TrackedDownloadStage.ImportFailed;
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
_eventAggregator.PublishEvent(new AlbumImportIncompleteEvent(trackedDownload));
return;
}
if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteAlbum.Albums.Count))
if (importResults.All(c => c.Result == ImportResultType.Imported)
|| importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteAlbum.Albums.Sum(x => x.AlbumReleases.Value.Where(y => y.Monitored).Sum(z => z.TrackCount))))
{
trackedDownload.State = TrackedDownloadStage.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
@ -120,14 +123,15 @@ namespace NzbDrone.Core.Download
if (importResults.Any(c => c.Result != ImportResultType.Imported))
{
trackedDownload.State = TrackedDownloadStage.ImportFailed;
var statusMessages = importResults
.Where(v => v.Result != ImportResultType.Imported)
.Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalTrack.Path), v.Errors))
.Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.Item.Path), v.Errors))
.ToArray();
trackedDownload.Warn(statusMessages);
_eventAggregator.PublishEvent(new AlbumImportIncompleteEvent(trackedDownload));
}
}
}
}

@ -152,7 +152,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads
private bool DownloadIsTrackable(TrackedDownload trackedDownload)
{
// If the download has already been imported or failed don't track it
if (trackedDownload.State != TrackedDownloadStage.Downloading)
if (trackedDownload.State == TrackedDownloadStage.DownloadFailed
|| trackedDownload.State == TrackedDownloadStage.Imported)
{
return false;
}

@ -36,8 +36,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads
public enum TrackedDownloadStage
{
Downloading,
Imported,
DownloadFailed
DownloadFailed,
Importing,
ImportFailed,
Imported
}
public enum TrackedDownloadStatus

@ -8,6 +8,7 @@ using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Download.TrackedDownloads
{
@ -104,7 +105,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads
if (historyItems.Any())
{
var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).First();
trackedDownload.State = GetStateFromHistory(firstHistoryItem.EventType);
trackedDownload.State = GetStateFromHistory(firstHistoryItem);
if (firstHistoryItem.EventType == HistoryEventType.AlbumImportIncomplete)
{
var messages = Json.Deserialize<List<TrackedDownloadStatusMessage>>(firstHistoryItem?.Data["statusMessages"]).ToArray();
trackedDownload.Warn(messages);
}
var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == HistoryEventType.Grabbed);
trackedDownload.Indexer = grabbedEvent?.Data["indexer"];
@ -186,7 +192,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
existingItem.CanBeRemoved != downloadItem.CanBeRemoved ||
existingItem.CanMoveFiles != downloadItem.CanMoveFiles)
{
_logger.Debug("Tracking '{0}:{1}': ClientState={2}{3} SonarrStage={4} Episode='{5}' OutputPath={6}.",
_logger.Debug("Tracking '{0}:{1}': ClientState={2}{3} LidarrStage={4} Album='{5}' OutputPath={6}.",
downloadItem.DownloadClient, downloadItem.Title,
downloadItem.Status, downloadItem.CanBeRemoved ? "" :
downloadItem.CanMoveFiles ? " (busy)" : " (readonly)",
@ -197,17 +203,25 @@ namespace NzbDrone.Core.Download.TrackedDownloads
}
private static TrackedDownloadStage GetStateFromHistory(HistoryEventType eventType)
private static TrackedDownloadStage GetStateFromHistory(NzbDrone.Core.History.History history)
{
switch (eventType)
switch (history.EventType)
{
case HistoryEventType.DownloadFolderImported:
case HistoryEventType.AlbumImportIncomplete:
return TrackedDownloadStage.ImportFailed;
case HistoryEventType.DownloadImported:
return TrackedDownloadStage.Imported;
case HistoryEventType.DownloadFailed:
return TrackedDownloadStage.DownloadFailed;
default:
return TrackedDownloadStage.Downloading;
}
// Since DownloadComplete is a new event type, we can't assume it exists for old downloads
if (history.EventType == HistoryEventType.TrackFileImported)
{
return DateTime.UtcNow.Subtract(history.Date).TotalSeconds < 60 ? TrackedDownloadStage.Importing : TrackedDownloadStage.Imported;
}
return TrackedDownloadStage.Downloading;
}
}
}

@ -21,7 +21,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
}
//Constructor for use when deserializing JSON
private TrackedDownloadStatusMessage()
public TrackedDownloadStatusMessage()
{
}
}

@ -6,22 +6,24 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
namespace NzbDrone.Core.Extras.Lyrics
{
public class ExistingLyricImporter : ImportExistingExtraFilesBase<LyricFile>
{
private readonly IExtraFileService<LyricFile> _lyricFileService;
private readonly IParsingService _parsingService;
private readonly IAugmentingService _augmentingService;
private readonly Logger _logger;
public ExistingLyricImporter(IExtraFileService<LyricFile> lyricFileService,
IParsingService parsingService,
Logger logger)
IAugmentingService augmentingService,
Logger logger)
: base (lyricFileService)
{
_lyricFileService = lyricFileService;
_parsingService = parsingService;
_augmentingService = augmentingService;
_logger = logger;
}
@ -34,29 +36,38 @@ namespace NzbDrone.Core.Extras.Lyrics
var subtitleFiles = new List<LyricFile>();
var filterResult = FilterAndClean(artist, filesOnDisk, importedFiles);
foreach (var possibleSubtitleFile in filterResult.FilesOnDisk)
foreach (var possibleLyricFile in filterResult.FilesOnDisk)
{
var extension = Path.GetExtension(possibleSubtitleFile);
var extension = Path.GetExtension(possibleLyricFile);
if (LyricFileExtensions.Extensions.Contains(extension))
{
var localTrack = _parsingService.GetLocalTrack(possibleSubtitleFile, artist);
var localTrack = new LocalTrack
{
FileTrackInfo = Parser.Parser.ParseMusicPath(possibleLyricFile),
Artist = artist,
Path = possibleLyricFile
};
if (localTrack == null)
try
{
_augmentingService.Augment(localTrack, false);
}
catch (AugmentingFailedException)
{
_logger.Debug("Unable to parse lyric file: {0}", possibleSubtitleFile);
_logger.Debug("Unable to parse lyric file: {0}", possibleLyricFile);
continue;
}
if (localTrack.Tracks.Empty())
{
_logger.Debug("Cannot find related tracks for: {0}", possibleSubtitleFile);
_logger.Debug("Cannot find related tracks for: {0}", possibleLyricFile);
continue;
}
if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1)
{
_logger.Debug("Lyric file: {0} does not match existing files.", possibleSubtitleFile);
_logger.Debug("Lyric file: {0} does not match existing files.", possibleLyricFile);
continue;
}
@ -65,8 +76,8 @@ namespace NzbDrone.Core.Extras.Lyrics
ArtistId = artist.Id,
AlbumId = localTrack.Album.Id,
TrackFileId = localTrack.Tracks.First().TrackFileId,
RelativePath = artist.Path.GetRelativePath(possibleSubtitleFile),
Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile),
RelativePath = artist.Path.GetRelativePath(possibleLyricFile),
Language = LanguageParser.ParseSubtitleLanguage(possibleLyricFile),
Extension = extension
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save