diff --git a/.gitignore b/.gitignore
index f25828171..46478856c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,10 @@ _dotCover*
# DevExpress CodeRush
src/.cr/
+# Emacs
+*~
+\#*\#
+
# NCrunch
*.ncrunch*
.*crunch*.local.xml
diff --git a/.travis.yml b/.travis.yml
index 943a90095..b57e34feb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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
diff --git a/build.sh b/build.sh
index 0b823b594..8cb3d6c22 100755
--- a/build.sh
+++ b/build.sh
@@ -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'
}
diff --git a/frontend/src/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js
index b2adf1171..9eb94eca3 100644
--- a/frontend/src/Activity/History/Details/HistoryDetails.js
+++ b/frontend/src/Activity/History/Details/HistoryDetails.js
@@ -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 (
+
@@ -362,6 +378,7 @@ class InteractiveImportModalContent extends Component {
artistId={selectedItem && selectedItem.artist && selectedItem.artist.id}
onModalClose={this.onSelectAlbumModalClose}
/>
+
);
}
@@ -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
};
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js
index ef0758e60..8112072c0 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js
@@ -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
};
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
index c40197767..e88d54ad1 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
@@ -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
diff --git a/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js b/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js
index cfec2c163..96f903bfa 100644
--- a/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js
+++ b/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js
@@ -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 (
- {title}
+ Manual Import - Select Track(s):
@@ -117,11 +151,18 @@ class SelectTrackModalContent extends Component {
{errorMessage}
}
+
+
{
isPopulated && !!items.length &&
@@ -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
};
diff --git a/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js b/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js
index a1d6f454d..35b17ade5 100644
--- a/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js
+++ b/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js
@@ -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,
diff --git a/frontend/src/InteractiveImport/Track/SelectTrackRow.js b/frontend/src/InteractiveImport/Track/SelectTrackRow.js
index 476dda5ad..f7dea7af3 100644
--- a/frontend/src/InteractiveImport/Track/SelectTrackRow.js
+++ b/frontend/src/InteractiveImport/Track/SelectTrackRow.js
@@ -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 (
-
+
@@ -51,6 +88,19 @@ class SelectTrackRow extends Component {
{title}
+
+
+ }
+ title={'Track status'}
+ body={iconTip}
+ position={tooltipPositions.LEFT}
+ />
+
);
}
@@ -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
};
diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js
index 9ff43b951..4aaf5540a 100644
--- a/frontend/src/Settings/MediaManagement/MediaManagement.js
+++ b/frontend/src/Settings/MediaManagement/MediaManagement.js
@@ -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 {
- Analyse audio files
+ Rescan Artist Folder after Refresh
@@ -229,16 +236,16 @@ class MediaManagement extends Component {
advancedSettings={advancedSettings}
isAdvanced={true}
>
- Rescan Artist Folder after Refresh
+ Allow Fingerprinting
diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js
index c080e12b9..9570f23d9 100644
--- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js
+++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js
@@ -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 = [
diff --git a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js
index 6d555bf23..d6a62d618 100644
--- a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js
+++ b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js
@@ -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) => {
diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js
index bf719f6c1..8c2232c0a 100644
--- a/frontend/src/Store/Actions/artistIndexActions.js
+++ b/frontend/src/Store/Actions/artistIndexActions.js
@@ -76,8 +76,7 @@ export const defaultState = {
name: 'artistType',
label: 'Type',
isSortable: true,
- isVisible: true,
- isModifiable: false
+ isVisible: true
},
{
name: 'qualityProfileId',
diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js
index 98c5203be..715e06562 100644
--- a/frontend/src/Store/Actions/historyActions.js
+++ b/frontend/src/Store/Actions/historyActions.js
@@ -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',
diff --git a/package.json b/package.json
index 23ba78bc3..ad0d05c3d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/Libraries/Fpcalc/chromaprint-fpcalc-1.4.3-macos-x86_64/fpcalc b/src/Libraries/Fpcalc/chromaprint-fpcalc-1.4.3-macos-x86_64/fpcalc
new file mode 100755
index 000000000..e8b3ae314
Binary files /dev/null and b/src/Libraries/Fpcalc/chromaprint-fpcalc-1.4.3-macos-x86_64/fpcalc differ
diff --git a/src/Libraries/Fpcalc/chromaprint-fpcalc-1.4.3-windows-x86_64/fpcalc.exe b/src/Libraries/Fpcalc/chromaprint-fpcalc-1.4.3-windows-x86_64/fpcalc.exe
new file mode 100755
index 000000000..94d7c81ec
Binary files /dev/null and b/src/Libraries/Fpcalc/chromaprint-fpcalc-1.4.3-windows-x86_64/fpcalc.exe differ
diff --git a/src/Libraries/MediaInfo/MediaInfo.dll b/src/Libraries/MediaInfo/MediaInfo.dll
deleted file mode 100644
index 24e6cb986..000000000
Binary files a/src/Libraries/MediaInfo/MediaInfo.dll and /dev/null differ
diff --git a/src/Libraries/MediaInfo/libmediainfo.0.dylib b/src/Libraries/MediaInfo/libmediainfo.0.dylib
deleted file mode 100644
index 5e5383ded..000000000
Binary files a/src/Libraries/MediaInfo/libmediainfo.0.dylib and /dev/null differ
diff --git a/src/Lidarr.Api.V1/Albums/AlbumModule.cs b/src/Lidarr.Api.V1/Albums/AlbumModule.cs
index 77ef981bf..1b19913f4 100644
--- a/src/Lidarr.Api.V1/Albums/AlbumModule.cs
+++ b/src/Lidarr.Api.V1/Albums/AlbumModule.cs
@@ -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,
+ IHandle,
+ IHandle,
+ IHandle
+
{
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());
+ }
}
}
diff --git a/src/Lidarr.Api.V1/Albums/AlbumModuleWithSignalR.cs b/src/Lidarr.Api.V1/Albums/AlbumModuleWithSignalR.cs
index e4a5f591b..48ffeb225 100644
--- a/src/Lidarr.Api.V1/Albums/AlbumModuleWithSignalR.cs
+++ b/src/Lidarr.Api.V1/Albums/AlbumModuleWithSignalR.cs
@@ -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,
- IHandle,
- IHandle,
- IHandle
+ public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR
{
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);
- // }
- //}
}
}
diff --git a/src/Lidarr.Api.V1/Artist/ArtistModule.cs b/src/Lidarr.Api.V1/Artist/ArtistModule.cs
index 5a0905703..faeaf0b06 100644
--- a/src/Lidarr.Api.V1/Artist/ArtistModule.cs
+++ b/src/Lidarr.Api.V1/Artist/ArtistModule.cs
@@ -24,7 +24,7 @@ using Lidarr.Http.Extensions;
namespace Lidarr.Api.V1.Artist
{
public class ArtistModule : LidarrRestModuleWithSignalR,
- IHandle,
+ IHandle,
IHandle,
IHandle,
IHandle,
@@ -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));
}
}
}
diff --git a/src/Lidarr.Api.V1/Artist/ArtistResource.cs b/src/Lidarr.Api.V1/Artist/ArtistResource.cs
index 88c5cb7fe..da77685c5 100644
--- a/src/Lidarr.Api.V1/Artist/ArtistResource.cs
+++ b/src/Lidarr.Api.V1/Artist/ArtistResource.cs
@@ -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,
diff --git a/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs b/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs
index 28a6dc5b7..072f0f2bc 100644
--- a/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs
+++ b/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs
@@ -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
};
}
}
diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs
index 3fc11ce3a..06b6e3f1c 100644
--- a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs
+++ b/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs
@@ -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>();
+ UpdateImportItems(resource);
+ return GetManualImportItems(resource.Select(x => x.Id)).AsResponse(HttpStatusCode.Accepted);
+ };
}
private List GetMediaFiles()
@@ -54,26 +59,29 @@ namespace Lidarr.Api.V1.ManualImport
return item;
}
- private void UpdateImportItem(ManualImportResource resource)
+ private void UpdateImportItems(List 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();
+ 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);
}
}
}
diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportModuleWithSignalR.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportModuleWithSignalR.cs
index 12a9cd6f6..87210a163 100644
--- a/src/Lidarr.Api.V1/ManualImport/ManualImportModuleWithSignalR.cs
+++ b/src/Lidarr.Api.V1/ManualImport/ManualImportModuleWithSignalR.cs
@@ -41,5 +41,10 @@ namespace Lidarr.Api.V1.ManualImport
{
return _manualImportService.Find(id).ToResource();
}
+
+ protected List GetManualImportItems(IEnumerable ids)
+ {
+ return ids.Select(x => _manualImportService.Find(x).ToResource()).ToList();
+ }
}
}
diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs
index cd3c60b17..52b07a183 100644
--- a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs
+++ b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs
@@ -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 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
};
}
diff --git a/src/Lidarr.Api.V1/TrackFiles/MediaInfoResource.cs b/src/Lidarr.Api.V1/TrackFiles/MediaInfoResource.cs
index a3b586585..69c60d739 100644
--- a/src/Lidarr.Api.V1/TrackFiles/MediaInfoResource.cs
+++ b/src/Lidarr.Api.V1/TrackFiles/MediaInfoResource.cs
@@ -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
{
diff --git a/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs b/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs
index 3de1db477..7f3ad420c 100644
--- a/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs
+++ b/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs
@@ -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 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();
- 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();
- 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));
}
}
diff --git a/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs b/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs
index d0ecefa37..0f19e9b24 100644
--- a/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs
+++ b/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs
@@ -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),
diff --git a/src/Lidarr.Api.V1/Tracks/TrackModule.cs b/src/Lidarr.Api.V1/Tracks/TrackModule.cs
index 454ffefd1..62d4863e7 100644
--- a/src/Lidarr.Api.V1/Tracks/TrackModule.cs
+++ b/src/Lidarr.Api.V1/Tracks/TrackModule.cs
@@ -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);
diff --git a/src/Lidarr.Api.V1/Tracks/TrackModuleWithSignalR.cs b/src/Lidarr.Api.V1/Tracks/TrackModuleWithSignalR.cs
index eefd9f1f7..cb94d5b60 100644
--- a/src/Lidarr.Api.V1/Tracks/TrackModuleWithSignalR.cs
+++ b/src/Lidarr.Api.V1/Tracks/TrackModuleWithSignalR.cs
@@ -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));
}
}
diff --git a/src/Lidarr.Api.V1/Tracks/TrackResource.cs b/src/Lidarr.Api.V1/Tracks/TrackResource.cs
index e07652d43..d1ebac96e 100644
--- a/src/Lidarr.Api.V1/Tracks/TrackResource.cs
+++ b/src/Lidarr.Api.V1/Tracks/TrackResource.cs
@@ -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,
diff --git a/src/Marr.Data/EntityGraph.cs b/src/Marr.Data/EntityGraph.cs
index 72d28dcdf..30d218d6f 100644
--- a/src/Marr.Data/EntityGraph.cs
+++ b/src/Marr.Data/EntityGraph.cs
@@ -165,7 +165,7 @@ namespace Marr.Data
///
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));
}
///
@@ -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);
}
diff --git a/src/Marr.Data/Mapping/RelationshipBuilder.cs b/src/Marr.Data/Mapping/RelationshipBuilder.cs
index b4926633f..7c9607ca2 100644
--- a/src/Marr.Data/Mapping/RelationshipBuilder.cs
+++ b/src/Marr.Data/Mapping/RelationshipBuilder.cs
@@ -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 LazyLoad(Func query, Func condition = null)
{
AssertCurrentPropertyIsSet();
+ var relationship = Relationships[_currentPropertyName];
- Relationships[_currentPropertyName].LazyLoaded = new LazyLoaded(query, condition);
+ relationship.LazyLoaded = new LazyLoaded(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;
}
diff --git a/src/Marr.Data/QGen/QueryBuilder.cs b/src/Marr.Data/QGen/QueryBuilder.cs
index ba135ac07..d5e798e4f 100644
--- a/src/Marr.Data/QGen/QueryBuilder.cs
+++ b/src/Marr.Data/QGen/QueryBuilder.cs
@@ -568,6 +568,23 @@ namespace Marr.Data.QGen
return Join(joinType, rightMember, filterExpression);
}
+ public virtual QueryBuilder Join(JoinType joinType, Expression>>> rightEntity, Expression> 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 Join(JoinType joinType, MemberInfo rightMember, Expression> filterExpression)
{
_isJoin = true;
diff --git a/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs b/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs
index 54b2a0e01..12b42a658 100644
--- a/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs
+++ b/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs
@@ -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 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;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Cache/Cached.cs b/src/NzbDrone.Common/Cache/Cached.cs
index 928809d30..572e1a560 100644
--- a/src/NzbDrone.Common/Cache/Cached.cs
+++ b/src/NzbDrone.Common/Cache/Cached.cs
@@ -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)
diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs
index 2eeb2fe4e..751e3dfca 100644
--- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs
+++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs
@@ -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(this IEnumerable 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(this IEnumerable source, Func 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 ToDictionaryIgnoreDuplicates(this IEnumerable src, Func keySelector)
{
var result = new Dictionary();
diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs
index dfdcb5e28..6ad4b6115 100644
--- a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs
+++ b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs
@@ -16,33 +16,27 @@ namespace NzbDrone.Core.Test.Datastore
[Test]
public void one_to_one()
{
- var trackFile = Builder.CreateNew()
+ var album = Builder.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