mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-22 14:33:30 -07:00
Merge branch 'develop' into mka-support
This commit is contained in:
commit
e4960194dc
279 changed files with 4487 additions and 2428 deletions
|
@ -9,7 +9,7 @@ variables:
|
||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '2.1.0'
|
majorVersion: '2.1.6'
|
||||||
minorVersion: $[counter('minorVersion', 1076)]
|
minorVersion: $[counter('minorVersion', 1076)]
|
||||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||||
|
|
|
@ -2,6 +2,8 @@ const loose = true;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'@babel/plugin-transform-logical-assignment-operators',
|
||||||
|
|
||||||
// Stage 1
|
// Stage 1
|
||||||
'@babel/plugin-proposal-export-default-from',
|
'@babel/plugin-proposal-export-default-from',
|
||||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||||
|
|
|
@ -3,9 +3,10 @@ import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType) {
|
function getIconName(eventType, data) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
|
@ -16,7 +17,7 @@ function getIconName(eventType) {
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'trackFileDeleted':
|
case 'trackFileDeleted':
|
||||||
return icons.DELETE;
|
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
||||||
case 'trackFileRenamed':
|
case 'trackFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
case 'trackFileRetagged':
|
case 'trackFileRetagged':
|
||||||
|
@ -54,11 +55,11 @@ function getTooltip(eventType, data) {
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return 'Album download failed';
|
return 'Album download failed';
|
||||||
case 'trackFileDeleted':
|
case 'trackFileDeleted':
|
||||||
return 'Track file deleted';
|
return data.reason === 'MissingFromDisk' ? translate('TrackFileMissingTooltip') : translate('TrackFileDeletedTooltip');
|
||||||
case 'trackFileRenamed':
|
case 'trackFileRenamed':
|
||||||
return 'Track file renamed';
|
return translate('TrackFileRenamedTooltip');
|
||||||
case 'trackFileRetagged':
|
case 'trackFileRetagged':
|
||||||
return 'Track file tags updated';
|
return translate('TrackFileTagsUpdatedTooltip');
|
||||||
case 'albumImportIncomplete':
|
case 'albumImportIncomplete':
|
||||||
return 'Files downloaded but not all could be imported';
|
return 'Files downloaded but not all could be imported';
|
||||||
case 'downloadImported':
|
case 'downloadImported':
|
||||||
|
@ -71,7 +72,7 @@ function getTooltip(eventType, data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, data }) {
|
function HistoryEventTypeCell({ eventType, data }) {
|
||||||
const iconName = getIconName(eventType);
|
const iconName = getIconName(eventType, data);
|
||||||
const iconKind = getIconKind(eventType);
|
const iconKind = getIconKind(eventType);
|
||||||
const tooltip = getTooltip(eventType, data);
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import QueueFilterModal from './QueueFilterModal';
|
import QueueFilterModal from './QueueFilterModal';
|
||||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
import QueueRowConnector from './QueueRowConnector';
|
||||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||||
|
|
||||||
class Queue extends Component {
|
class Queue extends Component {
|
||||||
|
|
||||||
|
@ -309,9 +309,16 @@ class Queue extends Component {
|
||||||
}
|
}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
|
|
||||||
<RemoveQueueItemsModal
|
<RemoveQueueItemModal
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
selectedCount={selectedCount}
|
selectedCount={selectedCount}
|
||||||
|
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.downloadClientHasPostImportCategory);
|
||||||
|
})
|
||||||
|
)}
|
||||||
canIgnore={isConfirmRemoveModalOpen && (
|
canIgnore={isConfirmRemoveModalOpen && (
|
||||||
selectedIds.every((id) => {
|
selectedIds.every((id) => {
|
||||||
const item = items.find((i) => i.id === id);
|
const item = items.find((i) => i.id === id);
|
||||||
|
@ -319,7 +326,7 @@ class Queue extends Component {
|
||||||
return !!(item && item.artistId && item.albumId);
|
return !!(item && item.artistId && item.albumId);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
allPending={isConfirmRemoveModalOpen && (
|
pending={isConfirmRemoveModalOpen && (
|
||||||
selectedIds.every((id) => {
|
selectedIds.every((id) => {
|
||||||
const item = items.find((i) => i.id === id);
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
|
|
@ -98,8 +98,10 @@ class QueueRow extends Component {
|
||||||
indexer,
|
indexer,
|
||||||
outputPath,
|
outputPath,
|
||||||
downloadClient,
|
downloadClient,
|
||||||
|
downloadClientHasPostImportCategory,
|
||||||
downloadForced,
|
downloadForced,
|
||||||
estimatedCompletionTime,
|
estimatedCompletionTime,
|
||||||
|
added,
|
||||||
timeleft,
|
timeleft,
|
||||||
size,
|
size,
|
||||||
sizeleft,
|
sizeleft,
|
||||||
|
@ -328,6 +330,15 @@ class QueueRow extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'added') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
date={added}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
@ -393,6 +404,7 @@ class QueueRow extends Component {
|
||||||
<RemoveQueueItemModal
|
<RemoveQueueItemModal
|
||||||
isOpen={isRemoveQueueItemModalOpen}
|
isOpen={isRemoveQueueItemModalOpen}
|
||||||
sourceTitle={title}
|
sourceTitle={title}
|
||||||
|
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||||
canIgnore={!!artist}
|
canIgnore={!!artist}
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||||
|
@ -422,8 +434,10 @@ QueueRow.propTypes = {
|
||||||
indexer: PropTypes.string,
|
indexer: PropTypes.string,
|
||||||
outputPath: PropTypes.string,
|
outputPath: PropTypes.string,
|
||||||
downloadClient: PropTypes.string,
|
downloadClient: PropTypes.string,
|
||||||
|
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||||
downloadForced: PropTypes.bool.isRequired,
|
downloadForced: PropTypes.bool.isRequired,
|
||||||
estimatedCompletionTime: PropTypes.string,
|
estimatedCompletionTime: PropTypes.string,
|
||||||
|
added: PropTypes.string,
|
||||||
timeleft: PropTypes.string,
|
timeleft: PropTypes.string,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
sizeleft: PropTypes.number,
|
sizeleft: PropTypes.number,
|
||||||
|
|
|
@ -1,175 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class RemoveQueueItemModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
removeFromClient: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = function() {
|
|
||||||
this.setState({
|
|
||||||
removeFromClient: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveFromClientChange = ({ value }) => {
|
|
||||||
this.setState({ removeFromClient: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlocklistChange = ({ value }) => {
|
|
||||||
this.setState({ blocklist: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSkipRedownloadChange = ({ value }) => {
|
|
||||||
this.setState({ skipRedownload: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
|
||||||
const state = this.state;
|
|
||||||
|
|
||||||
this.resetState();
|
|
||||||
this.props.onRemovePress(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.resetState();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
sourceTitle,
|
|
||||||
canIgnore,
|
|
||||||
isPending
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { removeFromClient, blocklist, skipRedownload } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
Remove - {sourceTitle}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPending ?
|
|
||||||
null :
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="removeFromClient"
|
|
||||||
value={removeFromClient}
|
|
||||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
|
||||||
isDisabled={!canIgnore}
|
|
||||||
onChange={this.onRemoveFromClientChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('BlocklistRelease')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="blocklist"
|
|
||||||
value={blocklist}
|
|
||||||
helpText={translate('BlocklistReleaseHelpText')}
|
|
||||||
onChange={this.onBlocklistChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
blocklist &&
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('SkipRedownload')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="skipRedownload"
|
|
||||||
value={skipRedownload}
|
|
||||||
helpText={translate('SkipRedownloadHelpText')}
|
|
||||||
onChange={this.onSkipRedownloadChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={this.onModalClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={this.onRemoveConfirmed}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveQueueItemModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
canIgnore: PropTypes.bool.isRequired,
|
|
||||||
isPending: PropTypes.bool.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RemoveQueueItemModal;
|
|
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './RemoveQueueItemModal.css';
|
||||||
|
|
||||||
|
interface RemovePressProps {
|
||||||
|
removeFromClient: boolean;
|
||||||
|
changeCategory: boolean;
|
||||||
|
blocklist: boolean;
|
||||||
|
skipRedownload: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoveQueueItemModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
sourceTitle: string;
|
||||||
|
canChangeCategory: boolean;
|
||||||
|
canIgnore: boolean;
|
||||||
|
isPending: boolean;
|
||||||
|
selectedCount?: number;
|
||||||
|
onRemovePress(props: RemovePressProps): void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||||
|
type BlocklistMethod =
|
||||||
|
| 'doNotBlocklist'
|
||||||
|
| 'blocklistAndSearch'
|
||||||
|
| 'blocklistOnly';
|
||||||
|
|
||||||
|
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
sourceTitle,
|
||||||
|
canIgnore,
|
||||||
|
canChangeCategory,
|
||||||
|
isPending,
|
||||||
|
selectedCount,
|
||||||
|
onRemovePress,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const multipleSelected = selectedCount && selectedCount > 1;
|
||||||
|
|
||||||
|
const [removalMethod, setRemovalMethod] =
|
||||||
|
useState<RemovalMethod>('removeFromClient');
|
||||||
|
const [blocklistMethod, setBlocklistMethod] =
|
||||||
|
useState<BlocklistMethod>('doNotBlocklist');
|
||||||
|
|
||||||
|
const { title, message } = useMemo(() => {
|
||||||
|
if (!selectedCount) {
|
||||||
|
return {
|
||||||
|
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||||
|
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCount === 1) {
|
||||||
|
return {
|
||||||
|
title: translate('RemoveSelectedItem'),
|
||||||
|
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: translate('RemoveSelectedItems'),
|
||||||
|
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||||
|
selectedCount,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, [sourceTitle, selectedCount]);
|
||||||
|
|
||||||
|
const removalMethodOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'removeFromClient',
|
||||||
|
value: translate('RemoveFromDownloadClient'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('RemoveMultipleFromDownloadClientHint')
|
||||||
|
: translate('RemoveFromDownloadClientHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'changeCategory',
|
||||||
|
value: translate('ChangeCategory'),
|
||||||
|
isDisabled: !canChangeCategory,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('ChangeCategoryMultipleHint')
|
||||||
|
: translate('ChangeCategoryHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ignore',
|
||||||
|
value: multipleSelected
|
||||||
|
? translate('IgnoreDownloads')
|
||||||
|
: translate('IgnoreDownload'),
|
||||||
|
isDisabled: !canIgnore,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('IgnoreDownloadsHint')
|
||||||
|
: translate('IgnoreDownloadHint'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||||
|
|
||||||
|
const blocklistMethodOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'doNotBlocklist',
|
||||||
|
value: translate('DoNotBlocklist'),
|
||||||
|
hint: translate('DoNotBlocklistHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocklistAndSearch',
|
||||||
|
value: translate('BlocklistAndSearch'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistAndSearchMultipleHint')
|
||||||
|
: translate('BlocklistAndSearchHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocklistOnly',
|
||||||
|
value: translate('BlocklistOnly'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistMultipleOnlyHint')
|
||||||
|
: translate('BlocklistOnlyHint'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [multipleSelected]);
|
||||||
|
|
||||||
|
const handleRemovalMethodChange = useCallback(
|
||||||
|
({ value }: { value: RemovalMethod }) => {
|
||||||
|
setRemovalMethod(value);
|
||||||
|
},
|
||||||
|
[setRemovalMethod]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlocklistMethodChange = useCallback(
|
||||||
|
({ value }: { value: BlocklistMethod }) => {
|
||||||
|
setBlocklistMethod(value);
|
||||||
|
},
|
||||||
|
[setBlocklistMethod]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirmRemove = useCallback(() => {
|
||||||
|
onRemovePress({
|
||||||
|
removeFromClient: removalMethod === 'removeFromClient',
|
||||||
|
changeCategory: removalMethod === 'changeCategory',
|
||||||
|
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||||
|
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemovalMethod('removeFromClient');
|
||||||
|
setBlocklistMethod('doNotBlocklist');
|
||||||
|
}, [
|
||||||
|
removalMethod,
|
||||||
|
blocklistMethod,
|
||||||
|
setRemovalMethod,
|
||||||
|
setBlocklistMethod,
|
||||||
|
onRemovePress,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
setRemovalMethod('removeFromClient');
|
||||||
|
setBlocklistMethod('doNotBlocklist');
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||||
|
<ModalContent onModalClose={handleModalClose}>
|
||||||
|
<ModalHeader>{title}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.message}>{message}</div>
|
||||||
|
|
||||||
|
{isPending ? null : (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="removalMethod"
|
||||||
|
value={removalMethod}
|
||||||
|
values={removalMethodOptions}
|
||||||
|
isDisabled={!canChangeCategory && !canIgnore}
|
||||||
|
helpTextWarning={translate(
|
||||||
|
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||||
|
)}
|
||||||
|
onChange={handleRemovalMethodChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{multipleSelected
|
||||||
|
? translate('BlocklistReleases')
|
||||||
|
: translate('BlocklistRelease')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="blocklistMethod"
|
||||||
|
value={blocklistMethod}
|
||||||
|
values={blocklistMethodOptions}
|
||||||
|
helpText={translate('BlocklistReleaseHelpText')}
|
||||||
|
onChange={handleBlocklistMethodChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||||
|
{translate('Remove')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoveQueueItemModal;
|
|
@ -1,176 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './RemoveQueueItemsModal.css';
|
|
||||||
|
|
||||||
class RemoveQueueItemsModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
removeFromClient: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = function() {
|
|
||||||
this.setState({
|
|
||||||
removeFromClient: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveFromClientChange = ({ value }) => {
|
|
||||||
this.setState({ removeFromClient: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlocklistChange = ({ value }) => {
|
|
||||||
this.setState({ blocklist: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSkipRedownloadChange = ({ value }) => {
|
|
||||||
this.setState({ skipRedownload: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
|
||||||
const state = this.state;
|
|
||||||
|
|
||||||
this.resetState();
|
|
||||||
this.props.onRemovePress(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.resetState();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
selectedCount,
|
|
||||||
canIgnore,
|
|
||||||
allPending
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { removeFromClient, blocklist, skipRedownload } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div className={styles.message}>
|
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', [selectedCount]) : translate('RemoveSelectedItemQueueMessageText')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
allPending ?
|
|
||||||
null :
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="removeFromClient"
|
|
||||||
value={removeFromClient}
|
|
||||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
|
||||||
isDisabled={!canIgnore}
|
|
||||||
onChange={this.onRemoveFromClientChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="blocklist"
|
|
||||||
value={blocklist}
|
|
||||||
helpText={translate('BlocklistReleaseHelpText')}
|
|
||||||
onChange={this.onBlocklistChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
blocklist &&
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('SkipRedownload')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="skipRedownload"
|
|
||||||
value={skipRedownload}
|
|
||||||
helpText={translate('SkipRedownloadHelpText')}
|
|
||||||
onChange={this.onSkipRedownloadChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={this.onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={this.onRemoveConfirmed}
|
|
||||||
>
|
|
||||||
{translate('Remove')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveQueueItemsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
selectedCount: PropTypes.number.isRequired,
|
|
||||||
canIgnore: PropTypes.bool.isRequired,
|
|
||||||
allPending: PropTypes.bool.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RemoveQueueItemsModal;
|
|
|
@ -58,8 +58,8 @@ class DeleteAlbumModalContent extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackFileCount,
|
trackFileCount = 0,
|
||||||
sizeOnDisk
|
sizeOnDisk = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const deleteFiles = this.state.deleteFiles;
|
const deleteFiles = this.state.deleteFiles;
|
||||||
|
@ -133,14 +133,14 @@ class DeleteAlbumModalContent extends Component {
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>
|
||||||
Close
|
{translate('Close')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={this.onDeleteAlbumConfirmed}
|
onPress={this.onDeleteAlbumConfirmed}
|
||||||
>
|
>
|
||||||
Delete
|
{translate('Delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -119,6 +119,7 @@
|
||||||
margin: 5px 10px 5px 0;
|
margin: 5px 10px 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.releaseDate,
|
||||||
.sizeOnDisk,
|
.sizeOnDisk,
|
||||||
.qualityProfileName,
|
.qualityProfileName,
|
||||||
.links,
|
.links,
|
||||||
|
@ -147,6 +148,12 @@
|
||||||
.headerContent {
|
.headerContent {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointLarge) {
|
@media only screen and (max-width: $breakpointLarge) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ interface CssExports {
|
||||||
'monitorToggleButton': string;
|
'monitorToggleButton': string;
|
||||||
'overview': string;
|
'overview': string;
|
||||||
'qualityProfileName': string;
|
'qualityProfileName': string;
|
||||||
|
'releaseDate': string;
|
||||||
'sizeOnDisk': string;
|
'sizeOnDisk': string;
|
||||||
'tags': string;
|
'tags': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
|
||||||
import AlbumInteractiveSearchModalConnector from 'Album/Search/AlbumInteractiveSearchModalConnector';
|
import AlbumInteractiveSearchModalConnector from 'Album/Search/AlbumInteractiveSearchModalConnector';
|
||||||
import ArtistGenres from 'Artist/Details/ArtistGenres';
|
import ArtistGenres from 'Artist/Details/ArtistGenres';
|
||||||
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import HeartRating from 'Components/HeartRating';
|
import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
@ -215,8 +216,8 @@ class AlbumDetails extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackFileCount,
|
trackFileCount = 0,
|
||||||
sizeOnDisk
|
sizeOnDisk = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -414,6 +415,7 @@ class AlbumDetails extends Component {
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
|
title={translate('ReleaseDate')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -421,10 +423,8 @@ class AlbumDetails extends Component {
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.sizeOnDisk}>
|
<span className={styles.releaseDate}>
|
||||||
{
|
{moment(releaseDate).format(shortDateFormat)}
|
||||||
moment(releaseDate).format(shortDateFormat)
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
|
@ -465,7 +465,7 @@ class AlbumDetails extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{monitored ? 'Monitored' : 'Unmonitored'}
|
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
|
@ -499,7 +499,7 @@ class AlbumDetails extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.links}>
|
<span className={styles.links}>
|
||||||
Links
|
{translate('Links')}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
|
@ -531,23 +531,24 @@ class AlbumDetails extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && albumsError &&
|
!isFetching && albumsError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingAlbumsFailed')}
|
{translate('AlbumsLoadError')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && trackFilesError &&
|
!isFetching && trackFilesError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingTrackFilesFailed')}
|
{translate('TrackFilesLoadError')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !!media.length &&
|
isPopulated && !!media.length &&
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
{
|
{
|
||||||
media.slice(0).map((medium) => {
|
media.slice(0).map((medium) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -70,6 +70,12 @@ function createMapStateToProps() {
|
||||||
isCommandExecuting(isSearchingCommand) &&
|
isCommandExecuting(isSearchingCommand) &&
|
||||||
isSearchingCommand.body.albumIds.indexOf(album.id) > -1
|
isSearchingCommand.body.albumIds.indexOf(album.id) > -1
|
||||||
);
|
);
|
||||||
|
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }));
|
||||||
|
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
|
||||||
|
const isRenamingArtist = (
|
||||||
|
isCommandExecuting(isRenamingArtistCommand) &&
|
||||||
|
isRenamingArtistCommand.body.artistIds.indexOf(artist.id) > -1
|
||||||
|
);
|
||||||
|
|
||||||
const isFetching = tracks.isFetching || isTrackFilesFetching;
|
const isFetching = tracks.isFetching || isTrackFilesFetching;
|
||||||
const isPopulated = tracks.isPopulated && isTrackFilesPopulated;
|
const isPopulated = tracks.isPopulated && isTrackFilesPopulated;
|
||||||
|
@ -80,6 +86,8 @@ function createMapStateToProps() {
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
artist,
|
artist,
|
||||||
isSearching,
|
isSearching,
|
||||||
|
isRenamingFiles,
|
||||||
|
isRenamingArtist,
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
tracksError,
|
tracksError,
|
||||||
|
@ -113,8 +121,27 @@ class AlbumDetailsConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) ||
|
const {
|
||||||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
|
id,
|
||||||
|
anyReleaseOk,
|
||||||
|
isRenamingFiles,
|
||||||
|
isRenamingArtist
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(prevProps.isRenamingFiles && !isRenamingFiles) ||
|
||||||
|
(prevProps.isRenamingArtist && !isRenamingArtist) ||
|
||||||
|
!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) ||
|
||||||
|
(prevProps.anyReleaseOk === false && anyReleaseOk === true)
|
||||||
|
) {
|
||||||
|
this.unpopulate();
|
||||||
|
this.populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the id has changed we need to clear the album
|
||||||
|
// files and fetch from the server.
|
||||||
|
|
||||||
|
if (prevProps.id !== id) {
|
||||||
this.unpopulate();
|
this.unpopulate();
|
||||||
this.populate();
|
this.populate();
|
||||||
}
|
}
|
||||||
|
@ -174,6 +201,8 @@ class AlbumDetailsConnector extends Component {
|
||||||
AlbumDetailsConnector.propTypes = {
|
AlbumDetailsConnector.propTypes = {
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
anyReleaseOk: PropTypes.bool,
|
anyReleaseOk: PropTypes.bool,
|
||||||
|
isRenamingFiles: PropTypes.bool.isRequired,
|
||||||
|
isRenamingArtist: PropTypes.bool.isRequired,
|
||||||
isAlbumFetching: PropTypes.bool,
|
isAlbumFetching: PropTypes.bool,
|
||||||
isAlbumPopulated: PropTypes.bool,
|
isAlbumPopulated: PropTypes.bool,
|
||||||
foreignAlbumId: PropTypes.string.isRequired,
|
foreignAlbumId: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration,
|
.duration,
|
||||||
|
.size,
|
||||||
.status {
|
.status {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|
1
frontend/src/Album/Details/TrackRow.css.d.ts
vendored
1
frontend/src/Album/Details/TrackRow.css.d.ts
vendored
|
@ -5,6 +5,7 @@ interface CssExports {
|
||||||
'customFormatScore': string;
|
'customFormatScore': string;
|
||||||
'duration': string;
|
'duration': string;
|
||||||
'monitored': string;
|
'monitored': string;
|
||||||
|
'size': string;
|
||||||
'status': string;
|
'status': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
'trackNumber': string;
|
'trackNumber': string;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { tooltipPositions } from 'Helpers/Props';
|
||||||
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
||||||
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import TrackActionsCell from './TrackActionsCell';
|
import TrackActionsCell from './TrackActionsCell';
|
||||||
import styles from './TrackRow.css';
|
import styles from './TrackRow.css';
|
||||||
|
@ -28,6 +29,7 @@ class TrackRow extends Component {
|
||||||
title,
|
title,
|
||||||
duration,
|
duration,
|
||||||
trackFilePath,
|
trackFilePath,
|
||||||
|
trackFileSize,
|
||||||
customFormats,
|
customFormats,
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
columns,
|
columns,
|
||||||
|
@ -145,6 +147,17 @@ class TrackRow extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'size') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.size}
|
||||||
|
>
|
||||||
|
{!!trackFileSize && formatBytes(trackFileSize)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'status') {
|
if (name === 'status') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
@ -192,6 +205,7 @@ TrackRow.propTypes = {
|
||||||
duration: PropTypes.number.isRequired,
|
duration: PropTypes.number.isRequired,
|
||||||
isSaving: PropTypes.bool,
|
isSaving: PropTypes.bool,
|
||||||
trackFilePath: PropTypes.string,
|
trackFilePath: PropTypes.string,
|
||||||
|
trackFileSize: PropTypes.number,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
mediaInfo: PropTypes.object,
|
mediaInfo: PropTypes.object,
|
||||||
|
|
|
@ -11,6 +11,7 @@ function createMapStateToProps() {
|
||||||
(id, trackFile) => {
|
(id, trackFile) => {
|
||||||
return {
|
return {
|
||||||
trackFilePath: trackFile ? trackFile.path : null,
|
trackFilePath: trackFile ? trackFile.path : null,
|
||||||
|
trackFileSize: trackFile ? trackFile.size : null,
|
||||||
customFormats: trackFile ? trackFile.customFormats : [],
|
customFormats: trackFile ? trackFile.customFormats : [],
|
||||||
customFormatScore: trackFile ? trackFile.customFormatScore : 0
|
customFormatScore: trackFile ? trackFile.customFormatScore : 0
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,6 +43,10 @@ class EditAlbumModalContent extends Component {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
trackFileCount = 0
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
monitored,
|
monitored,
|
||||||
anyReleaseOk,
|
anyReleaseOk,
|
||||||
|
@ -96,7 +100,7 @@ class EditAlbumModalContent extends Component {
|
||||||
type={inputTypes.ALBUM_RELEASE_SELECT}
|
type={inputTypes.ALBUM_RELEASE_SELECT}
|
||||||
name="releases"
|
name="releases"
|
||||||
helpText={translate('ReleasesHelpText')}
|
helpText={translate('ReleasesHelpText')}
|
||||||
isDisabled={anyReleaseOk.value && statistics.trackFileCount > 0}
|
isDisabled={anyReleaseOk.value && trackFileCount > 0}
|
||||||
albumReleases={releases}
|
albumReleases={releases}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function getTooltip(title, quality, size) {
|
function getTooltip(title, quality, size) {
|
||||||
if (!title) {
|
if (!title) {
|
||||||
|
@ -26,13 +27,44 @@ function getTooltip(title, quality, size) {
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function revisionLabel(className, quality, showRevision) {
|
||||||
|
if (!showRevision) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality.revision.isRepack) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
title={translate('Repack')}
|
||||||
|
>
|
||||||
|
R
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality.revision.version && quality.revision.version > 1) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
title={translate('Proper')}
|
||||||
|
>
|
||||||
|
P
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function TrackQuality(props) {
|
function TrackQuality(props) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
quality,
|
quality,
|
||||||
size,
|
size,
|
||||||
isCutoffNotMet
|
isCutoffNotMet,
|
||||||
|
showRevision
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (!quality) {
|
if (!quality) {
|
||||||
|
@ -40,13 +72,15 @@ function TrackQuality(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<span>
|
||||||
<Label
|
<Label
|
||||||
className={className}
|
className={className}
|
||||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
||||||
title={getTooltip(title, quality, size)}
|
title={getTooltip(title, quality, size)}
|
||||||
>
|
>
|
||||||
{quality.quality.name}
|
{quality.quality.name}
|
||||||
</Label>
|
</Label>{revisionLabel(className, quality, showRevision)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,11 +89,13 @@ TrackQuality.propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
isCutoffNotMet: PropTypes.bool
|
isCutoffNotMet: PropTypes.bool,
|
||||||
|
showRevision: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
TrackQuality.defaultProps = {
|
TrackQuality.defaultProps = {
|
||||||
title: ''
|
title: '',
|
||||||
|
showRevision: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TrackQuality;
|
export default TrackQuality;
|
||||||
|
|
|
@ -39,8 +39,17 @@ export interface CustomFilter {
|
||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionState {
|
||||||
|
dimensions: {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
albums: AlbumAppState;
|
albums: AlbumAppState;
|
||||||
|
app: AppSectionState;
|
||||||
artist: ArtistAppState;
|
artist: ArtistAppState;
|
||||||
artistIndex: ArtistIndexAppState;
|
artistIndex: ArtistIndexAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
|
|
|
@ -1,38 +1,10 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import Queue from 'typings/Queue';
|
||||||
import { QualityModel } from 'Quality/Quality';
|
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
Error,
|
Error,
|
||||||
} from './AppSectionState';
|
} from './AppSectionState';
|
||||||
|
|
||||||
export interface StatusMessage {
|
|
||||||
title: string;
|
|
||||||
messages: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Queue extends ModelBase {
|
|
||||||
quality: QualityModel;
|
|
||||||
customFormats: CustomFormat[];
|
|
||||||
size: number;
|
|
||||||
title: string;
|
|
||||||
sizeleft: number;
|
|
||||||
timeleft: string;
|
|
||||||
estimatedCompletionTime: string;
|
|
||||||
status: string;
|
|
||||||
trackedDownloadStatus: string;
|
|
||||||
trackedDownloadState: string;
|
|
||||||
statusMessages: StatusMessage[];
|
|
||||||
errorMessage: string;
|
|
||||||
downloadId: string;
|
|
||||||
protocol: string;
|
|
||||||
downloadClient: string;
|
|
||||||
outputPath: string;
|
|
||||||
artistId?: number;
|
|
||||||
albumId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||||
params: unknown;
|
params: unknown;
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,8 +56,8 @@ class DeleteArtistModalContent extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackFileCount,
|
trackFileCount = 0,
|
||||||
sizeOnDisk
|
sizeOnDisk = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const deleteFiles = this.state.deleteFiles;
|
const deleteFiles = this.state.deleteFiles;
|
||||||
|
|
|
@ -85,9 +85,9 @@ class AlbumRow extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackCount,
|
trackCount = 0,
|
||||||
trackFileCount,
|
trackFileCount = 0,
|
||||||
totalTrackCount
|
totalTrackCount = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -257,7 +257,8 @@ AlbumRow.propTypes = {
|
||||||
AlbumRow.defaultProps = {
|
AlbumRow.defaultProps = {
|
||||||
statistics: {
|
statistics: {
|
||||||
trackCount: 0,
|
trackCount: 0,
|
||||||
trackFileCount: 0
|
trackFileCount: 0,
|
||||||
|
totalTrackCount: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -161,6 +161,12 @@
|
||||||
.headerContent {
|
.headerContent {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointLarge) {
|
@media only screen and (max-width: $breakpointLarge) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||||
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
||||||
import MonitoringOptionsModal from 'Artist/MonitoringOptions/MonitoringOptionsModal';
|
import MonitoringOptionsModal from 'Artist/MonitoringOptions/MonitoringOptionsModal';
|
||||||
import ArtistInteractiveSearchModalConnector from 'Artist/Search/ArtistInteractiveSearchModalConnector';
|
import ArtistInteractiveSearchModalConnector from 'Artist/Search/ArtistInteractiveSearchModalConnector';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import HeartRating from 'Components/HeartRating';
|
import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
@ -221,8 +222,8 @@ class ArtistDetails extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackFileCount,
|
trackFileCount = 0,
|
||||||
sizeOnDisk
|
sizeOnDisk = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -241,7 +242,7 @@ class ArtistDetails extends Component {
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const continuing = status === 'continuing';
|
const continuing = status === 'continuing';
|
||||||
const endedString = artistType === 'Person' ? 'Deceased' : 'Inactive';
|
const endedString = artistType === 'Person' ? translate('Deceased') : translate('Inactive');
|
||||||
|
|
||||||
let trackFilesCountMessage = translate('TrackFilesCountMessage');
|
let trackFilesCountMessage = translate('TrackFilesCountMessage');
|
||||||
|
|
||||||
|
@ -555,7 +556,7 @@ class ArtistDetails extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.links}>
|
<span className={styles.links}>
|
||||||
Links
|
{translate('Links')}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
|
@ -611,17 +612,19 @@ class ArtistDetails extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && albumsError &&
|
!isFetching && albumsError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingAlbumsFailed')}
|
{translate('AlbumsLoadError')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && trackFilesError &&
|
!isFetching && trackFilesError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingTrackFilesFailed')}
|
{translate('TrackFilesLoadError')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -107,7 +107,6 @@ function createMapStateToProps() {
|
||||||
const isRefreshing = isArtistRefreshing || allArtistRefreshing;
|
const isRefreshing = isArtistRefreshing || allArtistRefreshing;
|
||||||
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id }));
|
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id }));
|
||||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }));
|
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }));
|
||||||
|
|
||||||
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
|
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
|
||||||
const isRenamingArtist = (
|
const isRenamingArtist = (
|
||||||
isCommandExecuting(isRenamingArtistCommand) &&
|
isCommandExecuting(isRenamingArtistCommand) &&
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
import ArtistHistoryModalContentConnector from './ArtistHistoryModalContentConnector';
|
import ArtistHistoryModalContentConnector from './ArtistHistoryModalContentConnector';
|
||||||
|
|
||||||
function ArtistHistoryModal(props) {
|
function ArtistHistoryModal(props) {
|
||||||
|
@ -13,6 +14,7 @@ function ArtistHistoryModal(props) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
<ArtistHistoryModalContentConnector
|
<ArtistHistoryModalContentConnector
|
||||||
|
|
|
@ -35,13 +35,9 @@ const columns = [
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'customFormats',
|
||||||
label: () => translate('Date'),
|
label: () => translate('CustomFormats'),
|
||||||
isVisible: true
|
isSortable: false,
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'details',
|
|
||||||
label: () => translate('Details'),
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -53,9 +49,13 @@ const columns = [
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
label: () => translate('Date'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
label: () => translate('Actions'),
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details,
|
|
||||||
.actions {
|
.actions {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'details': string;
|
|
||||||
'sourceTitle': string;
|
'sourceTitle': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|
|
@ -11,7 +11,6 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
@ -112,11 +111,19 @@ class ArtistHistoryRow extends Component {
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
<AlbumFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
date={date}
|
date={date}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRowCell className={styles.details}>
|
<TableRowCell className={styles.actions}>
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -134,25 +141,13 @@ class ArtistHistoryRow extends Component {
|
||||||
}
|
}
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
|
||||||
<Tooltip
|
|
||||||
anchor={formatCustomFormatScore(
|
|
||||||
customFormatScore,
|
|
||||||
customFormats.length
|
|
||||||
)}
|
|
||||||
tooltip={<AlbumFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
|
||||||
{
|
{
|
||||||
eventType === 'grabbed' &&
|
eventType === 'grabbed' &&
|
||||||
<IconButton
|
<IconButton
|
||||||
title={translate('MarkAsFailed')}
|
title={translate('MarkAsFailed')}
|
||||||
name={icons.REMOVE}
|
name={icons.REMOVE}
|
||||||
|
size={14}
|
||||||
onPress={this.onMarkAsFailedPress}
|
onPress={this.onMarkAsFailedPress}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,11 +201,15 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
||||||
|
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
const padding = bodyPaddingSmallScreen - 5;
|
const padding = bodyPaddingSmallScreen - 5;
|
||||||
|
const width = window.innerWidth - padding * 2;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
if (width !== size.width || height !== size.height) {
|
||||||
setSize({
|
setSize({
|
||||||
width: window.innerWidth - padding * 2,
|
width,
|
||||||
height: window.innerHeight,
|
height,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -213,13 +217,18 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
||||||
if (current) {
|
if (current) {
|
||||||
const width = current.clientWidth;
|
const width = current.clientWidth;
|
||||||
const padding = bodyPadding - 5;
|
const padding = bodyPadding - 5;
|
||||||
|
const finalWidth = width - padding * 2;
|
||||||
|
|
||||||
|
if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSize({
|
setSize({
|
||||||
width: width - padding * 2,
|
width: finalWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isSmallScreen, scrollerRef, bounds]);
|
}, [isSmallScreen, size, scrollerRef, bounds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||||
|
|
|
@ -33,7 +33,11 @@ function AlbumStudioAlbum(props: AlbumStudioAlbumProps) {
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { trackFileCount, totalTrackCount, percentOfTracks } = statistics;
|
const {
|
||||||
|
trackFileCount = 0,
|
||||||
|
totalTrackCount = 0,
|
||||||
|
percentOfTracks = 0,
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const onAlbumMonitoredPress = useCallback(() => {
|
const onAlbumMonitoredPress = useCallback(() => {
|
||||||
|
|
|
@ -215,6 +215,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
|
||||||
value={metadataProfileId}
|
value={metadataProfileId}
|
||||||
includeNoChange={true}
|
includeNoChange={true}
|
||||||
includeNoChangeDisabled={false}
|
includeNoChangeDisabled={false}
|
||||||
|
includeNone={true}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -47,7 +47,7 @@ class CalendarConnector extends Component {
|
||||||
gotoCalendarToday
|
gotoCalendarToday
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
registerPagePopulator(this.repopulate, ['trackFileUpdated', 'trackFileDeleted']);
|
||||||
|
|
||||||
if (useCurrentPage) {
|
if (useCurrentPage) {
|
||||||
fetchCalendar();
|
fetchCalendar();
|
||||||
|
|
|
@ -30,7 +30,9 @@ function CustomFiltersModalContent(props) {
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters.map((customFilter) => {
|
customFilters
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
.map((customFilter) => {
|
||||||
return (
|
return (
|
||||||
<CustomFilter
|
<CustomFilter
|
||||||
key={customFilter.id}
|
key={customFilter.id}
|
||||||
|
|
|
@ -276,6 +276,7 @@ FormInputGroup.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
values: PropTypes.arrayOf(PropTypes.any),
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
min: PropTypes.number,
|
min: PropTypes.number,
|
||||||
|
@ -289,6 +290,7 @@ FormInputGroup.propTypes = {
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
includeNoChange: PropTypes.bool,
|
includeNoChange: PropTypes.bool,
|
||||||
includeNoChangeDisabled: PropTypes.bool,
|
includeNoChangeDisabled: PropTypes.bool,
|
||||||
|
includeNone: PropTypes.bool,
|
||||||
selectedValueOptions: PropTypes.object,
|
selectedValueOptions: PropTypes.object,
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-right: $formLabelRightMarginWidth;
|
margin-right: $formLabelRightMarginWidth;
|
||||||
|
padding-top: 8px;
|
||||||
|
min-height: 35px;
|
||||||
|
text-align: end;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 35px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
|
|
|
@ -17,7 +17,6 @@ function createMapStateToProps() {
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
(state, { includeNone }) => includeNone,
|
(state, { includeNone }) => includeNone,
|
||||||
(metadataProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed, includeNone) => {
|
(metadataProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed, includeNone) => {
|
||||||
|
|
||||||
const profiles = metadataProfiles.items.filter((item) => item.name !== metadataProfileNames.NONE);
|
const profiles = metadataProfiles.items.filter((item) => item.name !== metadataProfileNames.NONE);
|
||||||
const noneProfile = metadataProfiles.items.find((item) => item.name === metadataProfileNames.NONE);
|
const noneProfile = metadataProfiles.items.find((item) => item.name === metadataProfileNames.NONE);
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,15 @@ class FilterMenuContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
customFilters.map((filter) => {
|
customFilters.length > 0 ?
|
||||||
|
<MenuItemSeparator /> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
customFilters
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
.map((filter) => {
|
||||||
return (
|
return (
|
||||||
<FilterMenuItem
|
<FilterMenuItem
|
||||||
key={filter.id}
|
key={filter.id}
|
||||||
|
|
|
@ -216,6 +216,8 @@ class SignalRConnector extends Component {
|
||||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||||
} else if (body.action === 'deleted') {
|
} else if (body.action === 'deleted') {
|
||||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||||
|
|
||||||
|
repopulatePage('trackFileDeleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repopulate the page to handle recently imported file
|
// Repopulate the page to handle recently imported file
|
||||||
|
|
|
@ -15,5 +15,5 @@
|
||||||
"start_url": "../../../../",
|
"start_url": "../../../../",
|
||||||
"theme_color": "#3a3f51",
|
"theme_color": "#3a3f51",
|
||||||
"background_color": "#3a3f51",
|
"background_color": "#3a3f51",
|
||||||
"display": "standalone"
|
"display": "minimal-ui"
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ import {
|
||||||
faEye as fasEye,
|
faEye as fasEye,
|
||||||
faFastBackward as fasFastBackward,
|
faFastBackward as fasFastBackward,
|
||||||
faFastForward as fasFastForward,
|
faFastForward as fasFastForward,
|
||||||
|
faFileCircleQuestion as fasFileCircleQuestion,
|
||||||
faFileExport as fasFileExport,
|
faFileExport as fasFileExport,
|
||||||
faFileImport as fasFileImport,
|
faFileImport as fasFileImport,
|
||||||
faFileInvoice as farFileInvoice,
|
faFileInvoice as farFileInvoice,
|
||||||
|
@ -154,7 +155,8 @@ export const EXPORT = fasFileExport;
|
||||||
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
||||||
export const FATAL = fasTimesCircle;
|
export const FATAL = fasTimesCircle;
|
||||||
export const FILE = farFile;
|
export const FILE = farFile;
|
||||||
export const FILEIMPORT = fasFileImport;
|
export const FILE_IMPORT = fasFileImport;
|
||||||
|
export const FILE_MISSING = fasFileCircleQuestion;
|
||||||
export const FILTER = fasFilter;
|
export const FILTER = fasFilter;
|
||||||
export const FOLDER = farFolder;
|
export const FOLDER = farFolder;
|
||||||
export const FOLDER_OPEN = fasFolderOpen;
|
export const FOLDER_OPEN = fasFolderOpen;
|
||||||
|
|
|
@ -44,9 +44,9 @@ class SelectAlbumRow extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackCount,
|
trackCount = 0,
|
||||||
trackFileCount,
|
trackFileCount = 0,
|
||||||
totalTrackCount
|
totalTrackCount = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const extendedTitle = disambiguation ? `${title} (${disambiguation})` : title;
|
const extendedTitle = disambiguation ? `${title} (${disambiguation})` : title;
|
||||||
|
@ -134,7 +134,8 @@ SelectAlbumRow.propTypes = {
|
||||||
SelectAlbumRow.defaultProps = {
|
SelectAlbumRow.defaultProps = {
|
||||||
statistics: {
|
statistics: {
|
||||||
trackCount: 0,
|
trackCount: 0,
|
||||||
trackFileCount: 0
|
trackFileCount: 0,
|
||||||
|
totalTrackCount: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -51,11 +51,11 @@ class SelectTrackRow extends Component {
|
||||||
iconKind = kinds.DEFAULT;
|
iconKind = kinds.DEFAULT;
|
||||||
iconTip = 'Track missing from library and no import selected.';
|
iconTip = 'Track missing from library and no import selected.';
|
||||||
} else if (importSelected && hasFile) {
|
} else if (importSelected && hasFile) {
|
||||||
iconName = icons.FILEIMPORT;
|
iconName = icons.FILE_IMPORT;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
iconTip = 'Warning: Existing track will be replaced by download.';
|
iconTip = 'Warning: Existing track will be replaced by download.';
|
||||||
} else if (importSelected && !hasFile) {
|
} else if (importSelected && !hasFile) {
|
||||||
iconName = icons.FILEIMPORT;
|
iconName = icons.FILE_IMPORT;
|
||||||
iconKind = kinds.DEFAULT;
|
iconKind = kinds.DEFAULT;
|
||||||
iconTip = 'Track missing from library and selected for import.';
|
iconTip = 'Track missing from library and selected for import.';
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,10 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quality {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.customFormatScore {
|
.customFormatScore {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|
|
@ -178,7 +178,7 @@ class InteractiveSearchRow extends Component {
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.quality}>
|
<TableRowCell className={styles.quality}>
|
||||||
<TrackQuality quality={quality} />
|
<TrackQuality quality={quality} showRevision={true} />
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
|
|
|
@ -89,7 +89,7 @@ class AddNewArtistSearchResult extends Component {
|
||||||
|
|
||||||
const linkProps = isExistingArtist ? { to: `/artist/${foreignArtistId}` } : { onPress: this.onPress };
|
const linkProps = isExistingArtist ? { to: `/artist/${foreignArtistId}` } : { onPress: this.onPress };
|
||||||
|
|
||||||
const endedString = artistType === 'Person' ? 'Deceased' : 'Inactive';
|
const endedString = artistType === 'Person' ? translate('Deceased') : translate('Inactive');
|
||||||
|
|
||||||
const height = calculateHeight(230, isSmallScreen);
|
const height = calculateHeight(230, isSmallScreen);
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ function EditSpecificationModalContent(props) {
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
fields && fields.some((x) => x.label === 'Regular Expression') &&
|
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
<div>
|
<div>
|
||||||
<div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
|
<div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
|
||||||
|
|
|
@ -139,7 +139,7 @@ class EditDownloadClientModalContent extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.NUMBER}
|
type={inputTypes.NUMBER}
|
||||||
name="priority"
|
name="priority"
|
||||||
helpText={translate('PriorityHelpText')}
|
helpText={translate('DownloadClientPriorityHelpText')}
|
||||||
min={1}
|
min={1}
|
||||||
max={50}
|
max={50}
|
||||||
{...priority}
|
{...priority}
|
||||||
|
|
|
@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteDownloadClients,
|
bulkDeleteDownloadClients,
|
||||||
bulkEditDownloadClients,
|
bulkEditDownloadClients,
|
||||||
|
setManageDownloadClientsSort,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
@ -80,6 +82,8 @@ const COLUMNS = [
|
||||||
|
|
||||||
interface ManageDownloadClientsModalContentProps {
|
interface ManageDownloadClientsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageDownloadClientsModalContent(
|
function ManageDownloadClientsModalContent(
|
||||||
|
@ -94,6 +98,8 @@ function ManageDownloadClientsModalContent(
|
||||||
isSaving,
|
isSaving,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
}: DownloadClientAppState = useSelector(
|
}: DownloadClientAppState = useSelector(
|
||||||
createClientSideCollectionSelector('settings.downloadClients')
|
createClientSideCollectionSelector('settings.downloadClients')
|
||||||
);
|
);
|
||||||
|
@ -114,6 +120,13 @@ function ManageDownloadClientsModalContent(
|
||||||
|
|
||||||
const selectedCount = selectedIds.length;
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
const onSortPress = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
dispatch(setManageDownloadClientsSort({ sortKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const onDeletePress = useCallback(() => {
|
const onDeletePress = useCallback(() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}, [setIsDeleteModalOpen]);
|
}, [setIsDeleteModalOpen]);
|
||||||
|
@ -219,6 +232,9 @@ function ManageDownloadClientsModalContent(
|
||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
onSelectAllChange={onSelectAllChange}
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
|
|
|
@ -61,10 +61,12 @@ function DownloadClientOptions(props) {
|
||||||
legend={translate('FailedDownloadHandling')}
|
legend={translate('FailedDownloadHandling')}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup
|
||||||
<FormLabel>
|
advancedSettings={advancedSettings}
|
||||||
{translate('Redownload')}
|
isAdvanced={true}
|
||||||
</FormLabel>
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
|
@ -74,7 +76,28 @@ function DownloadClientOptions(props) {
|
||||||
{...settings.autoRedownloadFailed}
|
{...settings.autoRedownloadFailed}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
settings.autoRedownloadFailed.value ?
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="autoRedownloadFailedFromInteractiveSearch"
|
||||||
|
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...settings.autoRedownloadFailedFromInteractiveSearch}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{translate('RemoveDownloadsAlert')}
|
{translate('RemoveDownloadsAlert')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
|
@ -8,11 +8,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.artistName {
|
.artistName {
|
||||||
flex: 0 0 300px;
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
flex: 0 1 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.foreignId {
|
.foreignId {
|
||||||
flex: 0 0 400px;
|
flex: 0 0 290px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.host {
|
.name {
|
||||||
flex: 0 0 300px;
|
flex: 0 1 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.path {
|
.foreignId {
|
||||||
flex: 0 0 400px;
|
flex: 0 0 290px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addImportListExclusion {
|
.addImportListExclusion {
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'addButton': string;
|
'addButton': string;
|
||||||
'addImportListExclusion': string;
|
'addImportListExclusion': string;
|
||||||
'host': string;
|
'foreignId': string;
|
||||||
'importListExclusionsHeader': string;
|
'importListExclusionsHeader': string;
|
||||||
'path': string;
|
'name': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -51,8 +51,10 @@ class ImportListExclusions extends Component {
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div className={styles.importListExclusionsHeader}>
|
<div className={styles.importListExclusionsHeader}>
|
||||||
<div className={styles.host}>{translate('Name')}</div>
|
<div className={styles.name}>
|
||||||
<div className={styles.path}>
|
{translate('Name')}
|
||||||
|
</div>
|
||||||
|
<div className={styles.foreignId}>
|
||||||
{translate('ForeignId')}
|
{translate('ForeignId')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteIndexers,
|
bulkDeleteIndexers,
|
||||||
bulkEditIndexers,
|
bulkEditIndexers,
|
||||||
|
setManageIndexersSort,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
@ -80,6 +82,8 @@ const COLUMNS = [
|
||||||
|
|
||||||
interface ManageIndexersModalContentProps {
|
interface ManageIndexersModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
@ -92,6 +96,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
isSaving,
|
isSaving,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
}: IndexerAppState = useSelector(
|
}: IndexerAppState = useSelector(
|
||||||
createClientSideCollectionSelector('settings.indexers')
|
createClientSideCollectionSelector('settings.indexers')
|
||||||
);
|
);
|
||||||
|
@ -112,6 +118,13 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
|
||||||
const selectedCount = selectedIds.length;
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
const onSortPress = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
dispatch(setManageIndexersSort({ sortKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const onDeletePress = useCallback(() => {
|
const onDeletePress = useCallback(() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}, [setIsDeleteModalOpen]);
|
}, [setIsDeleteModalOpen]);
|
||||||
|
@ -214,6 +227,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
onSelectAllChange={onSelectAllChange}
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
|
|
|
@ -265,26 +265,24 @@ class MediaManagement extends Component {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
settings.importExtraFiles.value &&
|
settings.importExtraFiles.value ?
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
>
|
>
|
||||||
<FormLabel>
|
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
|
||||||
{translate('ImportExtraFiles')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="extraFileExtensions"
|
name="extraFileExtensions"
|
||||||
helpTexts={[
|
helpTexts={[
|
||||||
translate('ExtraFileExtensionsHelpTexts1'),
|
translate('ExtraFileExtensionsHelpText'),
|
||||||
translate('ExtraFileExtensionsHelpTexts2')
|
translate('ExtraFileExtensionsHelpTextsExamples')
|
||||||
]}
|
]}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.extraFileExtensions}
|
{...settings.extraFileExtensions}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> : null
|
||||||
}
|
}
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,8 +60,9 @@ class Notification extends Component {
|
||||||
onReleaseImport,
|
onReleaseImport,
|
||||||
onUpgrade,
|
onUpgrade,
|
||||||
onRename,
|
onRename,
|
||||||
onAlbumDelete,
|
onArtistAdd,
|
||||||
onArtistDelete,
|
onArtistDelete,
|
||||||
|
onAlbumDelete,
|
||||||
onHealthIssue,
|
onHealthIssue,
|
||||||
onHealthRestored,
|
onHealthRestored,
|
||||||
onDownloadFailure,
|
onDownloadFailure,
|
||||||
|
@ -72,8 +73,9 @@ class Notification extends Component {
|
||||||
supportsOnReleaseImport,
|
supportsOnReleaseImport,
|
||||||
supportsOnUpgrade,
|
supportsOnUpgrade,
|
||||||
supportsOnRename,
|
supportsOnRename,
|
||||||
supportsOnAlbumDelete,
|
supportsOnArtistAdd,
|
||||||
supportsOnArtistDelete,
|
supportsOnArtistDelete,
|
||||||
|
supportsOnAlbumDelete,
|
||||||
supportsOnHealthIssue,
|
supportsOnHealthIssue,
|
||||||
supportsOnHealthRestored,
|
supportsOnHealthRestored,
|
||||||
supportsOnDownloadFailure,
|
supportsOnDownloadFailure,
|
||||||
|
@ -95,59 +97,75 @@ class Notification extends Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnGrab && onGrab &&
|
supportsOnGrab && onGrab ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
{translate('OnGrab')}
|
{translate('OnGrab')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnReleaseImport && onReleaseImport &&
|
supportsOnReleaseImport && onReleaseImport ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
{translate('OnReleaseImport')}
|
{translate('OnReleaseImport')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnUpgrade && onReleaseImport && onUpgrade &&
|
supportsOnUpgrade && onReleaseImport && onUpgrade ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
{translate('OnUpgrade')}
|
{translate('OnUpgrade')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnRename && onRename &&
|
supportsOnRename && onRename ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
{translate('OnRename')}
|
{translate('OnRename')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnTrackRetag && onTrackRetag &&
|
supportsOnTrackRetag && onTrackRetag ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
{translate('OnTrackRetag')}
|
{translate('OnTrackRetag')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnAlbumDelete && onAlbumDelete &&
|
supportsOnArtistAdd && onArtistAdd ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
{translate('OnAlbumDelete')}
|
{translate('OnArtistAdd')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnArtistDelete && onArtistDelete &&
|
supportsOnArtistDelete && onArtistDelete ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
{translate('OnArtistDelete')}
|
{translate('OnArtistDelete')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnHealthIssue && onHealthIssue &&
|
supportsOnAlbumDelete && onAlbumDelete ?
|
||||||
|
<Label kind={kinds.SUCCESS}>
|
||||||
|
{translate('OnAlbumDelete')}
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
supportsOnHealthIssue && onHealthIssue ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
{translate('OnHealthIssue')}
|
{translate('OnHealthIssue')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -159,35 +177,38 @@ class Notification extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnDownloadFailure && onDownloadFailure &&
|
supportsOnDownloadFailure && onDownloadFailure ?
|
||||||
<Label kind={kinds.SUCCESS} >
|
<Label kind={kinds.SUCCESS} >
|
||||||
{translate('OnDownloadFailure')}
|
{translate('OnDownloadFailure')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnImportFailure && onImportFailure &&
|
supportsOnImportFailure && onImportFailure ?
|
||||||
<Label kind={kinds.SUCCESS} >
|
<Label kind={kinds.SUCCESS} >
|
||||||
{translate('OnImportFailure')}
|
{translate('OnImportFailure')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
supportsOnApplicationUpdate && onApplicationUpdate &&
|
supportsOnApplicationUpdate && onApplicationUpdate ?
|
||||||
<Label kind={kinds.SUCCESS} >
|
<Label kind={kinds.SUCCESS} >
|
||||||
{translate('OnApplicationUpdate')}
|
{translate('OnApplicationUpdate')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!onGrab && !onReleaseImport && !onRename && !onTrackRetag && !onAlbumDelete && !onArtistDelete &&
|
!onGrab && !onReleaseImport && !onRename && !onTrackRetag && !onArtistAdd && !onArtistDelete && !onAlbumDelete && !onHealthIssue && !onHealthRestored && !onDownloadFailure && !onImportFailure && !onApplicationUpdate ?
|
||||||
!onHealthIssue && !onHealthRestored && !onDownloadFailure && !onImportFailure && !onApplicationUpdate &&
|
|
||||||
<Label
|
<Label
|
||||||
kind={kinds.DISABLED}
|
kind={kinds.DISABLED}
|
||||||
outline={true}
|
outline={true}
|
||||||
>
|
>
|
||||||
{translate('Disabled')}
|
{translate('Disabled')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<TagList
|
<TagList
|
||||||
|
@ -223,8 +244,9 @@ Notification.propTypes = {
|
||||||
onReleaseImport: PropTypes.bool.isRequired,
|
onReleaseImport: PropTypes.bool.isRequired,
|
||||||
onUpgrade: PropTypes.bool.isRequired,
|
onUpgrade: PropTypes.bool.isRequired,
|
||||||
onRename: PropTypes.bool.isRequired,
|
onRename: PropTypes.bool.isRequired,
|
||||||
onAlbumDelete: PropTypes.bool.isRequired,
|
onArtistAdd: PropTypes.bool.isRequired,
|
||||||
onArtistDelete: PropTypes.bool.isRequired,
|
onArtistDelete: PropTypes.bool.isRequired,
|
||||||
|
onAlbumDelete: PropTypes.bool.isRequired,
|
||||||
onHealthIssue: PropTypes.bool.isRequired,
|
onHealthIssue: PropTypes.bool.isRequired,
|
||||||
onHealthRestored: PropTypes.bool.isRequired,
|
onHealthRestored: PropTypes.bool.isRequired,
|
||||||
onDownloadFailure: PropTypes.bool.isRequired,
|
onDownloadFailure: PropTypes.bool.isRequired,
|
||||||
|
@ -235,8 +257,9 @@ Notification.propTypes = {
|
||||||
supportsOnReleaseImport: PropTypes.bool.isRequired,
|
supportsOnReleaseImport: PropTypes.bool.isRequired,
|
||||||
supportsOnUpgrade: PropTypes.bool.isRequired,
|
supportsOnUpgrade: PropTypes.bool.isRequired,
|
||||||
supportsOnRename: PropTypes.bool.isRequired,
|
supportsOnRename: PropTypes.bool.isRequired,
|
||||||
supportsOnAlbumDelete: PropTypes.bool.isRequired,
|
supportsOnArtistAdd: PropTypes.bool.isRequired,
|
||||||
supportsOnArtistDelete: PropTypes.bool.isRequired,
|
supportsOnArtistDelete: PropTypes.bool.isRequired,
|
||||||
|
supportsOnAlbumDelete: PropTypes.bool.isRequired,
|
||||||
supportsOnHealthIssue: PropTypes.bool.isRequired,
|
supportsOnHealthIssue: PropTypes.bool.isRequired,
|
||||||
supportsOnHealthRestored: PropTypes.bool.isRequired,
|
supportsOnHealthRestored: PropTypes.bool.isRequired,
|
||||||
supportsOnDownloadFailure: PropTypes.bool.isRequired,
|
supportsOnDownloadFailure: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -19,8 +19,9 @@ function NotificationEventItems(props) {
|
||||||
onReleaseImport,
|
onReleaseImport,
|
||||||
onUpgrade,
|
onUpgrade,
|
||||||
onRename,
|
onRename,
|
||||||
onAlbumDelete,
|
onArtistAdd,
|
||||||
onArtistDelete,
|
onArtistDelete,
|
||||||
|
onAlbumDelete,
|
||||||
onHealthIssue,
|
onHealthIssue,
|
||||||
onHealthRestored,
|
onHealthRestored,
|
||||||
onDownloadFailure,
|
onDownloadFailure,
|
||||||
|
@ -31,8 +32,9 @@ function NotificationEventItems(props) {
|
||||||
supportsOnReleaseImport,
|
supportsOnReleaseImport,
|
||||||
supportsOnUpgrade,
|
supportsOnUpgrade,
|
||||||
supportsOnRename,
|
supportsOnRename,
|
||||||
supportsOnAlbumDelete,
|
supportsOnArtistAdd,
|
||||||
supportsOnArtistDelete,
|
supportsOnArtistDelete,
|
||||||
|
supportsOnAlbumDelete,
|
||||||
supportsOnHealthIssue,
|
supportsOnHealthIssue,
|
||||||
supportsOnHealthRestored,
|
supportsOnHealthRestored,
|
||||||
includeHealthWarnings,
|
includeHealthWarnings,
|
||||||
|
@ -57,7 +59,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onGrab"
|
name="onGrab"
|
||||||
helpText={translate('OnGrabHelpText')}
|
helpText={translate('OnGrab')}
|
||||||
isDisabled={!supportsOnGrab.value}
|
isDisabled={!supportsOnGrab.value}
|
||||||
{...onGrab}
|
{...onGrab}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -68,7 +70,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onReleaseImport"
|
name="onReleaseImport"
|
||||||
helpText={translate('OnReleaseImportHelpText')}
|
helpText={translate('OnReleaseImport')}
|
||||||
isDisabled={!supportsOnReleaseImport.value}
|
isDisabled={!supportsOnReleaseImport.value}
|
||||||
{...onReleaseImport}
|
{...onReleaseImport}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -81,7 +83,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onUpgrade"
|
name="onUpgrade"
|
||||||
helpText={translate('OnUpgradeHelpText')}
|
helpText={translate('OnUpgrade')}
|
||||||
isDisabled={!supportsOnUpgrade.value}
|
isDisabled={!supportsOnUpgrade.value}
|
||||||
{...onUpgrade}
|
{...onUpgrade}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -93,7 +95,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onDownloadFailure"
|
name="onDownloadFailure"
|
||||||
helpText={translate('OnDownloadFailureHelpText')}
|
helpText={translate('OnDownloadFailure')}
|
||||||
isDisabled={!supportsOnDownloadFailure.value}
|
isDisabled={!supportsOnDownloadFailure.value}
|
||||||
{...onDownloadFailure}
|
{...onDownloadFailure}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -104,7 +106,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onImportFailure"
|
name="onImportFailure"
|
||||||
helpText={translate('OnImportFailureHelpText')}
|
helpText={translate('OnImportFailure')}
|
||||||
isDisabled={!supportsOnImportFailure.value}
|
isDisabled={!supportsOnImportFailure.value}
|
||||||
{...onImportFailure}
|
{...onImportFailure}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -115,7 +117,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onRename"
|
name="onRename"
|
||||||
helpText={translate('OnRenameHelpText')}
|
helpText={translate('OnRename')}
|
||||||
isDisabled={!supportsOnRename.value}
|
isDisabled={!supportsOnRename.value}
|
||||||
{...onRename}
|
{...onRename}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -126,7 +128,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onTrackRetag"
|
name="onTrackRetag"
|
||||||
helpText={translate('OnTrackRetagHelpText')}
|
helpText={translate('OnTrackRetag')}
|
||||||
isDisabled={!supportsOnTrackRetag.value}
|
isDisabled={!supportsOnTrackRetag.value}
|
||||||
{...onTrackRetag}
|
{...onTrackRetag}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -136,10 +138,10 @@ function NotificationEventItems(props) {
|
||||||
<div>
|
<div>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onAlbumDelete"
|
name="onArtistAdd"
|
||||||
helpText={translate('OnAlbumDeleteHelpText')}
|
helpText={translate('OnArtistAdd')}
|
||||||
isDisabled={!supportsOnAlbumDelete.value}
|
isDisabled={!supportsOnArtistAdd.value}
|
||||||
{...onAlbumDelete}
|
{...onArtistAdd}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -148,18 +150,29 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onArtistDelete"
|
name="onArtistDelete"
|
||||||
helpText={translate('OnArtistDeleteHelpText')}
|
helpText={translate('OnArtistDelete')}
|
||||||
isDisabled={!supportsOnArtistDelete.value}
|
isDisabled={!supportsOnArtistDelete.value}
|
||||||
{...onArtistDelete}
|
{...onArtistDelete}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="onAlbumDelete"
|
||||||
|
helpText={translate('OnAlbumDelete')}
|
||||||
|
isDisabled={!supportsOnAlbumDelete.value}
|
||||||
|
{...onAlbumDelete}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onApplicationUpdate"
|
name="onApplicationUpdate"
|
||||||
helpText={translate('OnApplicationUpdateHelpText')}
|
helpText={translate('OnApplicationUpdate')}
|
||||||
isDisabled={!supportsOnApplicationUpdate.value}
|
isDisabled={!supportsOnApplicationUpdate.value}
|
||||||
{...onApplicationUpdate}
|
{...onApplicationUpdate}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -170,7 +183,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onHealthIssue"
|
name="onHealthIssue"
|
||||||
helpText={translate('OnHealthIssueHelpText')}
|
helpText={translate('OnHealthIssue')}
|
||||||
isDisabled={!supportsOnHealthIssue.value}
|
isDisabled={!supportsOnHealthIssue.value}
|
||||||
{...onHealthIssue}
|
{...onHealthIssue}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -181,7 +194,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onHealthRestored"
|
name="onHealthRestored"
|
||||||
helpText={translate('OnHealthRestoredHelpText')}
|
helpText={translate('OnHealthRestored')}
|
||||||
isDisabled={!supportsOnHealthRestored.value}
|
isDisabled={!supportsOnHealthRestored.value}
|
||||||
{...onHealthRestored}
|
{...onHealthRestored}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -194,7 +207,7 @@ function NotificationEventItems(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="includeHealthWarnings"
|
name="includeHealthWarnings"
|
||||||
helpText={translate('IncludeHealthWarningsHelpText')}
|
helpText={translate('IncludeHealthWarnings')}
|
||||||
isDisabled={!supportsOnHealthIssue.value}
|
isDisabled={!supportsOnHealthIssue.value}
|
||||||
{...includeHealthWarnings}
|
{...includeHealthWarnings}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
background-color: var(--sliderAccentColor);
|
background-color: var(--sliderAccentColor);
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
|
|
||||||
&:nth-child(odd) {
|
&:nth-child(3n+1) {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
.kilobitsPerSecond {
|
.kilobitsPerSecond {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex: 0 0 250px;
|
flex: 0 0 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sizeInput {
|
.sizeInput {
|
||||||
|
|
|
@ -50,21 +50,24 @@ class QualityDefinition extends Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
sliderMinSize: getSliderValue(props.minSize, slider.min),
|
sliderMinSize: getSliderValue(props.minSize, slider.min),
|
||||||
sliderMaxSize: getSliderValue(props.maxSize, slider.max)
|
sliderMaxSize: getSliderValue(props.maxSize, slider.max),
|
||||||
|
sliderPreferredSize: getSliderValue(props.preferredSize, (slider.max - 3))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onSliderChange = ([sliderMinSize, sliderMaxSize]) => {
|
onSliderChange = ([sliderMinSize, sliderPreferredSize, sliderMaxSize]) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
sliderMinSize,
|
sliderMinSize,
|
||||||
sliderMaxSize
|
sliderMaxSize,
|
||||||
|
sliderPreferredSize
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.onSizeChange({
|
this.props.onSizeChange({
|
||||||
minSize: roundNumber(Math.pow(sliderMinSize, 1.1)),
|
minSize: roundNumber(Math.pow(sliderMinSize, 1.1)),
|
||||||
|
preferredSize: sliderPreferredSize === (slider.max - 3) ? null : roundNumber(Math.pow(sliderPreferredSize, 1.1)),
|
||||||
maxSize: sliderMaxSize === slider.max ? null : roundNumber(Math.pow(sliderMaxSize, 1.1))
|
maxSize: sliderMaxSize === slider.max ? null : roundNumber(Math.pow(sliderMaxSize, 1.1))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -72,12 +75,14 @@ class QualityDefinition extends Component {
|
||||||
onAfterSliderChange = () => {
|
onAfterSliderChange = () => {
|
||||||
const {
|
const {
|
||||||
minSize,
|
minSize,
|
||||||
maxSize
|
maxSize,
|
||||||
|
preferredSize
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
sliderMiSize: getSliderValue(minSize, slider.min),
|
sliderMiSize: getSliderValue(minSize, slider.min),
|
||||||
sliderMaxSize: getSliderValue(maxSize, slider.max)
|
sliderMaxSize: getSliderValue(maxSize, slider.max),
|
||||||
|
sliderPreferredSize: getSliderValue(preferredSize, (slider.max - 3)) // fix
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,7 +95,22 @@ class QualityDefinition extends Component {
|
||||||
|
|
||||||
this.props.onSizeChange({
|
this.props.onSizeChange({
|
||||||
minSize,
|
minSize,
|
||||||
maxSize: this.props.maxSize
|
maxSize: this.props.maxSize,
|
||||||
|
preferredSize: this.props.preferredSize
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onPreferredSizeChange = ({ value }) => {
|
||||||
|
const preferredSize = value === (MAX - 3) ? null : getValue(value);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
sliderPreferredSize: getSliderValue(preferredSize, slider.preferred)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.onSizeChange({
|
||||||
|
minSize: this.props.minSize,
|
||||||
|
maxSize: this.props.maxSize,
|
||||||
|
preferredSize
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -103,7 +123,8 @@ class QualityDefinition extends Component {
|
||||||
|
|
||||||
this.props.onSizeChange({
|
this.props.onSizeChange({
|
||||||
minSize: this.props.minSize,
|
minSize: this.props.minSize,
|
||||||
maxSize
|
maxSize,
|
||||||
|
preferredSize: this.props.preferredSize
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -117,20 +138,25 @@ class QualityDefinition extends Component {
|
||||||
title,
|
title,
|
||||||
minSize,
|
minSize,
|
||||||
maxSize,
|
maxSize,
|
||||||
|
preferredSize,
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
onTitleChange
|
onTitleChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
sliderMinSize,
|
sliderMinSize,
|
||||||
sliderMaxSize
|
sliderMaxSize,
|
||||||
|
sliderPreferredSize
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const minBytes = minSize * 128;
|
const minBytes = minSize * 128;
|
||||||
const maxBytes = maxSize && maxSize * 128;
|
|
||||||
|
|
||||||
const minRate = `${formatBytes(minBytes, true)}/s`;
|
const minRate = `${formatBytes(minBytes, true)}/s`;
|
||||||
const maxRate = maxBytes ? `${formatBytes(maxBytes, true)}/s` : 'Unlimited';
|
|
||||||
|
const preferredBytes = preferredSize * 128;
|
||||||
|
const preferredRate = preferredBytes ? `${formatBytes(preferredBytes, true)}/s` : translate('Unlimited');
|
||||||
|
|
||||||
|
const maxBytes = maxSize && maxSize * 128;
|
||||||
|
const maxRate = maxBytes ? `${formatBytes(maxBytes, true)}/s` : translate('Unlimited');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.qualityDefinition}>
|
<div className={styles.qualityDefinition}>
|
||||||
|
@ -151,9 +177,10 @@ class QualityDefinition extends Component {
|
||||||
min={slider.min}
|
min={slider.min}
|
||||||
max={slider.max}
|
max={slider.max}
|
||||||
step={slider.step}
|
step={slider.step}
|
||||||
minDistance={MIN_DISTANCE * 5}
|
minDistance={3}
|
||||||
value={[sliderMinSize, sliderMaxSize]}
|
value={[sliderMinSize, sliderPreferredSize, sliderMaxSize]}
|
||||||
withTracks={true}
|
withTracks={true}
|
||||||
|
allowCross={false}
|
||||||
snapDragDisabled={true}
|
snapDragDisabled={true}
|
||||||
className={styles.slider}
|
className={styles.slider}
|
||||||
trackClassName={styles.bar}
|
trackClassName={styles.bar}
|
||||||
|
@ -172,7 +199,23 @@ class QualityDefinition extends Component {
|
||||||
body={
|
body={
|
||||||
<QualityDefinitionLimits
|
<QualityDefinitionLimits
|
||||||
bytes={minBytes}
|
bytes={minBytes}
|
||||||
message={translate('NoMinimumForAnyRuntime')}
|
message={translate('NoMinimumForAnyDuration')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Label kind={kinds.SUCCESS}>{preferredRate}</Label>
|
||||||
|
}
|
||||||
|
title={translate('PreferredSize')}
|
||||||
|
body={
|
||||||
|
<QualityDefinitionLimits
|
||||||
|
bytes={preferredBytes}
|
||||||
|
message={translate('NoLimitForAnyDuration')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
position={tooltipPositions.BOTTOM}
|
position={tooltipPositions.BOTTOM}
|
||||||
|
@ -188,7 +231,7 @@ class QualityDefinition extends Component {
|
||||||
body={
|
body={
|
||||||
<QualityDefinitionLimits
|
<QualityDefinitionLimits
|
||||||
bytes={maxBytes}
|
bytes={maxBytes}
|
||||||
message={translate('NoLimitForAnyRuntime')}
|
message={translate('NoLimitForAnyDuration')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
position={tooltipPositions.BOTTOM}
|
position={tooltipPositions.BOTTOM}
|
||||||
|
@ -201,14 +244,14 @@ class QualityDefinition extends Component {
|
||||||
advancedSettings &&
|
advancedSettings &&
|
||||||
<div className={styles.kilobitsPerSecond}>
|
<div className={styles.kilobitsPerSecond}>
|
||||||
<div>
|
<div>
|
||||||
Min
|
{translate('Min')}
|
||||||
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
className={styles.sizeInput}
|
className={styles.sizeInput}
|
||||||
name={`${id}.min`}
|
name={`${id}.min`}
|
||||||
value={minSize || MIN}
|
value={minSize || MIN}
|
||||||
min={MIN}
|
min={MIN}
|
||||||
max={maxSize ? maxSize - MIN_DISTANCE : MAX - MIN_DISTANCE}
|
max={preferredSize ? preferredSize - 5 : MAX - 5}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
isFloat={true}
|
isFloat={true}
|
||||||
onChange={this.onMinSizeChange}
|
onChange={this.onMinSizeChange}
|
||||||
|
@ -216,7 +259,22 @@ class QualityDefinition extends Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Max
|
{translate('Preferred')}
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
className={styles.sizeInput}
|
||||||
|
name={`${id}.min`}
|
||||||
|
value={preferredSize || MAX - 5}
|
||||||
|
min={MIN}
|
||||||
|
max={maxSize ? maxSize - 5 : MAX - 5}
|
||||||
|
step={0.1}
|
||||||
|
isFloat={true}
|
||||||
|
onChange={this.onPreferredSizeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{translate('Max')}
|
||||||
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
className={styles.sizeInput}
|
className={styles.sizeInput}
|
||||||
|
@ -242,6 +300,7 @@ QualityDefinition.propTypes = {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
minSize: PropTypes.number,
|
minSize: PropTypes.number,
|
||||||
maxSize: PropTypes.number,
|
maxSize: PropTypes.number,
|
||||||
|
preferredSize: PropTypes.number,
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
advancedSettings: PropTypes.bool.isRequired,
|
||||||
onTitleChange: PropTypes.func.isRequired,
|
onTitleChange: PropTypes.func.isRequired,
|
||||||
onSizeChange: PropTypes.func.isRequired
|
onSizeChange: PropTypes.func.isRequired
|
||||||
|
|
|
@ -23,11 +23,12 @@ class QualityDefinitionConnector extends Component {
|
||||||
this.props.setQualityDefinitionValue({ id: this.props.id, name: 'title', value });
|
this.props.setQualityDefinitionValue({ id: this.props.id, name: 'title', value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSizeChange = ({ minSize, maxSize }) => {
|
onSizeChange = ({ minSize, maxSize, preferredSize }) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
minSize: currentMinSize,
|
minSize: currentMinSize,
|
||||||
maxSize: currentMaxSize
|
maxSize: currentMaxSize,
|
||||||
|
preferredSize: currentPreferredSize
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (minSize !== currentMinSize) {
|
if (minSize !== currentMinSize) {
|
||||||
|
@ -37,6 +38,10 @@ class QualityDefinitionConnector extends Component {
|
||||||
if (maxSize !== currentMaxSize) {
|
if (maxSize !== currentMaxSize) {
|
||||||
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
|
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preferredSize !== currentPreferredSize) {
|
||||||
|
this.props.setQualityDefinitionValue({ id, name: 'preferredSize', value: preferredSize });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -57,6 +62,7 @@ QualityDefinitionConnector.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
minSize: PropTypes.number,
|
minSize: PropTypes.number,
|
||||||
maxSize: PropTypes.number,
|
maxSize: PropTypes.number,
|
||||||
|
preferredSize: PropTypes.number,
|
||||||
setQualityDefinitionValue: PropTypes.func.isRequired,
|
setQualityDefinitionValue: PropTypes.func.isRequired,
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
||||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||||
|
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
|
@ -33,6 +35,7 @@ export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestD
|
||||||
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
||||||
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
|
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
|
||||||
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
||||||
|
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -49,6 +52,7 @@ export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT)
|
||||||
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
|
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
|
||||||
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
|
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
|
||||||
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
||||||
|
export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT);
|
||||||
|
|
||||||
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
|
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
|
@ -88,7 +92,14 @@ export default {
|
||||||
isTesting: false,
|
isTesting: false,
|
||||||
isTestingAll: false,
|
isTestingAll: false,
|
||||||
items: [],
|
items: [],
|
||||||
pendingChanges: {}
|
pendingChanges: {},
|
||||||
|
sortKey: 'name',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
sortPredicates: {
|
||||||
|
name: function(item) {
|
||||||
|
return item.name.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -122,7 +133,10 @@ export default {
|
||||||
|
|
||||||
return selectedSchema;
|
return selectedSchema;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
||||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||||
|
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
|
@ -36,6 +38,7 @@ export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
|
||||||
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
||||||
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
|
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
|
||||||
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
||||||
|
export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -53,6 +56,7 @@ export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
|
||||||
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
|
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
|
||||||
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
|
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
|
||||||
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
||||||
|
export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT);
|
||||||
|
|
||||||
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
|
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
|
@ -92,7 +96,14 @@ export default {
|
||||||
isTesting: false,
|
isTesting: false,
|
||||||
isTestingAll: false,
|
isTestingAll: false,
|
||||||
items: [],
|
items: [],
|
||||||
pendingChanges: {}
|
pendingChanges: {},
|
||||||
|
sortKey: 'name',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
sortPredicates: {
|
||||||
|
name: function(item) {
|
||||||
|
return item.name.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -142,7 +153,13 @@ export default {
|
||||||
delete selectedSchema.name;
|
delete selectedSchema.name;
|
||||||
|
|
||||||
selectedSchema.fields = selectedSchema.fields.map((field) => {
|
selectedSchema.fields = selectedSchema.fields.map((field) => {
|
||||||
return { ...field };
|
const newField = { ...field };
|
||||||
|
|
||||||
|
if (newField.privacy === 'apiKey' || newField.privacy === 'password') {
|
||||||
|
newField.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return newField;
|
||||||
});
|
});
|
||||||
|
|
||||||
newState.selectedSchema = selectedSchema;
|
newState.selectedSchema = selectedSchema;
|
||||||
|
@ -153,7 +170,10 @@ export default {
|
||||||
};
|
};
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
return updateSectionState(state, section, newState);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -107,6 +107,8 @@ export default {
|
||||||
selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport;
|
selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport;
|
||||||
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
|
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
|
||||||
selectedSchema.onRename = selectedSchema.supportsOnRename;
|
selectedSchema.onRename = selectedSchema.supportsOnRename;
|
||||||
|
selectedSchema.onArtistAdd = selectedSchema.supportsOnArtistAdd;
|
||||||
|
selectedSchema.onArtistDelete = selectedSchema.supportsOnArtistDelete;
|
||||||
selectedSchema.onHealthIssue = selectedSchema.supportsOnHealthIssue;
|
selectedSchema.onHealthIssue = selectedSchema.supportsOnHealthIssue;
|
||||||
selectedSchema.onDownloadFailure = selectedSchema.supportsOnDownloadFailure;
|
selectedSchema.onDownloadFailure = selectedSchema.supportsOnDownloadFailure;
|
||||||
selectedSchema.onImportFailure = selectedSchema.supportsOnImportFailure;
|
selectedSchema.onImportFailure = selectedSchema.supportsOnImportFailure;
|
||||||
|
|
|
@ -176,7 +176,7 @@ export const defaultState = {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackCount = 0,
|
trackCount = 0,
|
||||||
trackFileCount
|
trackFileCount = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const progress = trackCount ? trackFileCount / trackCount * 100 : 100;
|
const progress = trackCount ? trackFileCount / trackCount * 100 : 100;
|
||||||
|
@ -201,7 +201,7 @@ export const defaultState = {
|
||||||
albumCount: function(item) {
|
albumCount: function(item) {
|
||||||
const { statistics = {} } = item;
|
const { statistics = {} } = item;
|
||||||
|
|
||||||
return statistics.albumCount;
|
return statistics.albumCount || 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
trackCount: function(item) {
|
trackCount: function(item) {
|
||||||
|
@ -229,7 +229,7 @@ export const defaultState = {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackCount = 0,
|
trackCount = 0,
|
||||||
trackFileCount
|
trackFileCount = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const progress = trackCount ?
|
const progress = trackCount ?
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const section = 'interactiveImport';
|
||||||
|
|
||||||
const albumsSection = `${section}.albums`;
|
const albumsSection = `${section}.albums`;
|
||||||
const trackFilesSection = `${section}.trackFiles`;
|
const trackFilesSection = `${section}.trackFiles`;
|
||||||
|
let abortCurrentFetchRequest = null;
|
||||||
let abortCurrentRequest = null;
|
let abortCurrentRequest = null;
|
||||||
let currentIds = [];
|
let currentIds = [];
|
||||||
|
|
||||||
|
@ -35,6 +36,8 @@ export const defaultState = {
|
||||||
pendingChanges: {},
|
pendingChanges: {},
|
||||||
sortKey: 'path',
|
sortKey: 'path',
|
||||||
sortDirection: sortDirections.ASCENDING,
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
secondarySortKey: 'path',
|
||||||
|
secondarySortDirection: sortDirections.ASCENDING,
|
||||||
recentFolders: [],
|
recentFolders: [],
|
||||||
importMode: 'chooseImportMode',
|
importMode: 'chooseImportMode',
|
||||||
sortPredicates: {
|
sortPredicates: {
|
||||||
|
@ -75,6 +78,8 @@ export const defaultState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
|
'interactiveImport.sortKey',
|
||||||
|
'interactiveImport.sortDirection',
|
||||||
'interactiveImport.recentFolders',
|
'interactiveImport.recentFolders',
|
||||||
'interactiveImport.importMode'
|
'interactiveImport.importMode'
|
||||||
];
|
];
|
||||||
|
@ -123,6 +128,11 @@ export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_I
|
||||||
// Action Handlers
|
// Action Handlers
|
||||||
export const actionHandlers = handleThunks({
|
export const actionHandlers = handleThunks({
|
||||||
[FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) {
|
[FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) {
|
||||||
|
if (abortCurrentFetchRequest) {
|
||||||
|
abortCurrentFetchRequest();
|
||||||
|
abortCurrentFetchRequest = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!payload.downloadId && !payload.folder) {
|
if (!payload.downloadId && !payload.folder) {
|
||||||
dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } }));
|
dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } }));
|
||||||
return;
|
return;
|
||||||
|
@ -130,12 +140,14 @@ export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
dispatch(set({ section, isFetching: true }));
|
dispatch(set({ section, isFetching: true }));
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const { request, abortRequest } = createAjaxRequest({
|
||||||
url: '/manualimport',
|
url: '/manualimport',
|
||||||
data: payload
|
data: payload
|
||||||
}).request;
|
});
|
||||||
|
|
||||||
promise.done((data) => {
|
abortCurrentFetchRequest = abortRequest;
|
||||||
|
|
||||||
|
request.done((data) => {
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
update({ section, data }),
|
update({ section, data }),
|
||||||
|
|
||||||
|
@ -148,7 +160,11 @@ export const actionHandlers = handleThunks({
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
request.fail((xhr) => {
|
||||||
|
if (xhr.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
|
|
|
@ -146,6 +146,12 @@ export const defaultState = {
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'added',
|
||||||
|
label: () => translate('Added'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'progress',
|
name: 'progress',
|
||||||
label: () => translate('Progress'),
|
label: () => translate('Progress'),
|
||||||
|
@ -406,13 +412,14 @@ export const actionHandlers = handleThunks({
|
||||||
id,
|
id,
|
||||||
removeFromClient,
|
removeFromClient,
|
||||||
blocklist,
|
blocklist,
|
||||||
skipRedownload
|
skipRedownload,
|
||||||
|
changeCategory
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
dispatch(updateItem({ section: paged, id, isRemoving: true }));
|
dispatch(updateItem({ section: paged, id, isRemoving: true }));
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url: `/queue/${id}?removeFromClient=${removeFromClient}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
|
url: `/queue/${id}?removeFromClient=${removeFromClient}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
}).request;
|
}).request;
|
||||||
|
|
||||||
|
@ -430,7 +437,8 @@ export const actionHandlers = handleThunks({
|
||||||
ids,
|
ids,
|
||||||
removeFromClient,
|
removeFromClient,
|
||||||
blocklist,
|
blocklist,
|
||||||
skipRedownload
|
skipRedownload,
|
||||||
|
changeCategory
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
|
@ -446,7 +454,7 @@ export const actionHandlers = handleThunks({
|
||||||
]));
|
]));
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url: `/queue/bulk?removeFromClient=${removeFromClient}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
|
url: `/queue/bulk?removeFromClient=${removeFromClient}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
|
|
|
@ -58,6 +58,11 @@ export const defaultState = {
|
||||||
label: () => translate('AudioInfo'),
|
label: () => translate('AudioInfo'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'size',
|
||||||
|
label: () => translate('Size'),
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'customFormats',
|
name: 'customFormats',
|
||||||
label: 'Formats',
|
label: 'Formats',
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
function createDimensionsSelector() {
|
function createDimensionsSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.app.dimensions,
|
(state: AppState) => state.app.dimensions,
|
||||||
(dimensions) => {
|
(dimensions) => {
|
||||||
return dimensions;
|
return dimensions;
|
||||||
}
|
}
|
|
@ -22,9 +22,9 @@ class About extends Component {
|
||||||
isNetCore,
|
isNetCore,
|
||||||
isDocker,
|
isDocker,
|
||||||
runtimeVersion,
|
runtimeVersion,
|
||||||
migrationVersion,
|
|
||||||
databaseVersion,
|
databaseVersion,
|
||||||
databaseType,
|
databaseType,
|
||||||
|
migrationVersion,
|
||||||
appData,
|
appData,
|
||||||
startupPath,
|
startupPath,
|
||||||
mode,
|
mode,
|
||||||
|
@ -66,13 +66,13 @@ class About extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('DBMigration')}
|
title={translate('Database')}
|
||||||
data={migrationVersion}
|
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('Database')}
|
title={translate('DatabaseMigration')}
|
||||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
data={migrationVersion}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
@ -114,9 +114,9 @@ About.propTypes = {
|
||||||
isNetCore: PropTypes.bool.isRequired,
|
isNetCore: PropTypes.bool.isRequired,
|
||||||
runtimeVersion: PropTypes.string.isRequired,
|
runtimeVersion: PropTypes.string.isRequired,
|
||||||
isDocker: PropTypes.bool.isRequired,
|
isDocker: PropTypes.bool.isRequired,
|
||||||
migrationVersion: PropTypes.number.isRequired,
|
|
||||||
databaseType: PropTypes.string.isRequired,
|
databaseType: PropTypes.string.isRequired,
|
||||||
databaseVersion: PropTypes.string.isRequired,
|
databaseVersion: PropTypes.string.isRequired,
|
||||||
|
migrationVersion: PropTypes.number.isRequired,
|
||||||
appData: PropTypes.string.isRequired,
|
appData: PropTypes.string.isRequired,
|
||||||
startupPath: PropTypes.string.isRequired,
|
startupPath: PropTypes.string.isRequired,
|
||||||
mode: PropTypes.string.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -53,7 +53,7 @@ class CutoffUnmetConnector extends Component {
|
||||||
gotoCutoffUnmetFirstPage
|
gotoCutoffUnmetFirstPage
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate, ['trackFileUpdated']);
|
registerPagePopulator(this.repopulate, ['trackFileUpdated', 'trackFileDeleted']);
|
||||||
|
|
||||||
if (useCurrentPage) {
|
if (useCurrentPage) {
|
||||||
fetchCutoffUnmet();
|
fetchCutoffUnmet();
|
||||||
|
|
|
@ -50,7 +50,7 @@ class MissingConnector extends Component {
|
||||||
gotoMissingFirstPage
|
gotoMissingFirstPage
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate, ['trackFileUpdated']);
|
registerPagePopulator(this.repopulate, ['trackFileUpdated', 'trackFileDeleted']);
|
||||||
|
|
||||||
if (useCurrentPage) {
|
if (useCurrentPage) {
|
||||||
fetchMissing();
|
fetchMissing();
|
||||||
|
|
32
frontend/src/typings/Queue.ts
Normal file
32
frontend/src/typings/Queue.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
|
||||||
|
export interface StatusMessage {
|
||||||
|
title: string;
|
||||||
|
messages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Queue extends ModelBase {
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats: CustomFormat[];
|
||||||
|
size: number;
|
||||||
|
title: string;
|
||||||
|
sizeleft: number;
|
||||||
|
timeleft: string;
|
||||||
|
estimatedCompletionTime: string;
|
||||||
|
added?: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus: string;
|
||||||
|
trackedDownloadState: string;
|
||||||
|
statusMessages: StatusMessage[];
|
||||||
|
errorMessage: string;
|
||||||
|
downloadId: string;
|
||||||
|
protocol: string;
|
||||||
|
downloadClient: string;
|
||||||
|
outputPath: string;
|
||||||
|
artistId?: number;
|
||||||
|
albumId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Queue;
|
|
@ -145,7 +145,7 @@ namespace Lidarr.Api.V1.Artist
|
||||||
|
|
||||||
MapCoversToLocal(artistsResources.ToArray());
|
MapCoversToLocal(artistsResources.ToArray());
|
||||||
LinkNextPreviousAlbums(artistsResources.ToArray());
|
LinkNextPreviousAlbums(artistsResources.ToArray());
|
||||||
LinkArtistStatistics(artistsResources, artistStats);
|
LinkArtistStatistics(artistsResources, artistStats.ToDictionary(x => x.ArtistId));
|
||||||
artistsResources.ForEach(LinkRootFolderPath);
|
artistsResources.ForEach(LinkRootFolderPath);
|
||||||
|
|
||||||
// PopulateAlternateTitles(seriesResources);
|
// PopulateAlternateTitles(seriesResources);
|
||||||
|
@ -219,19 +219,16 @@ namespace Lidarr.Api.V1.Artist
|
||||||
LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id));
|
LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LinkArtistStatistics(List<ArtistResource> resources, List<ArtistStatistics> artistStatistics)
|
private void LinkArtistStatistics(List<ArtistResource> resources, Dictionary<int, ArtistStatistics> artistStatistics)
|
||||||
{
|
{
|
||||||
foreach (var artist in resources)
|
foreach (var artist in resources)
|
||||||
{
|
{
|
||||||
var stats = artistStatistics.SingleOrDefault(ss => ss.ArtistId == artist.Id);
|
if (artistStatistics.TryGetValue(artist.Id, out var stats))
|
||||||
if (stats == null)
|
|
||||||
{
|
{
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
LinkArtistStatistics(artist, stats);
|
LinkArtistStatistics(artist, stats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void LinkArtistStatistics(ArtistResource resource, ArtistStatistics artistStatistics)
|
private void LinkArtistStatistics(ArtistResource resource, ArtistStatistics artistStatistics)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
namespace Lidarr.Api.V1.Config
|
namespace Lidarr.Api.V1.Config
|
||||||
|
@ -8,6 +8,7 @@ namespace Lidarr.Api.V1.Config
|
||||||
public string DownloadClientWorkingFolders { get; set; }
|
public string DownloadClientWorkingFolders { get; set; }
|
||||||
public bool EnableCompletedDownloadHandling { get; set; }
|
public bool EnableCompletedDownloadHandling { get; set; }
|
||||||
public bool AutoRedownloadFailed { get; set; }
|
public bool AutoRedownloadFailed { get; set; }
|
||||||
|
public bool AutoRedownloadFailedFromInteractiveSearch { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DownloadClientConfigResourceMapper
|
public static class DownloadClientConfigResourceMapper
|
||||||
|
@ -19,7 +20,8 @@ namespace Lidarr.Api.V1.Config
|
||||||
DownloadClientWorkingFolders = model.DownloadClientWorkingFolders,
|
DownloadClientWorkingFolders = model.DownloadClientWorkingFolders,
|
||||||
|
|
||||||
EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling,
|
EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling,
|
||||||
AutoRedownloadFailed = model.AutoRedownloadFailed
|
AutoRedownloadFailed = model.AutoRedownloadFailed,
|
||||||
|
AutoRedownloadFailedFromInteractiveSearch = model.AutoRedownloadFailedFromInteractiveSearch
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,15 +68,14 @@ namespace Lidarr.Api.V1.History
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, bool includeArtist, bool includeAlbum, bool includeTrack, int? eventType, int? albumId, string downloadId, [FromQuery] int[] artistIds = null, [FromQuery] int[] quality = null)
|
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, bool includeArtist, bool includeAlbum, bool includeTrack, [FromQuery(Name = "eventType")] int[] eventTypes, int? albumId, string downloadId, [FromQuery] int[] artistIds = null, [FromQuery] int[] quality = null)
|
||||||
{
|
{
|
||||||
var pagingResource = new PagingResource<HistoryResource>(paging);
|
var pagingResource = new PagingResource<HistoryResource>(paging);
|
||||||
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, EntityHistory>("date", SortDirection.Descending);
|
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, EntityHistory>("date", SortDirection.Descending);
|
||||||
|
|
||||||
if (eventType.HasValue)
|
if (eventTypes != null && eventTypes.Any())
|
||||||
{
|
{
|
||||||
var filterValue = (EntityHistoryEventType)eventType.Value;
|
pagingSpec.FilterExpressions.Add(v => eventTypes.Contains((int)v.EventType));
|
||||||
pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (albumId.HasValue)
|
if (albumId.HasValue)
|
||||||
|
|
|
@ -127,7 +127,7 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to parse albums in the release");
|
throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to parse albums in the release");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _downloadService.DownloadReport(remoteAlbum);
|
await _downloadService.DownloadReport(remoteAlbum, release.DownloadClientId);
|
||||||
}
|
}
|
||||||
catch (ReleaseDownloadException ex)
|
catch (ReleaseDownloadException ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -21,6 +21,7 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
private readonly IMakeDownloadDecision _downloadDecisionMaker;
|
private readonly IMakeDownloadDecision _downloadDecisionMaker;
|
||||||
private readonly IProcessDownloadDecisions _downloadDecisionProcessor;
|
private readonly IProcessDownloadDecisions _downloadDecisionProcessor;
|
||||||
private readonly IIndexerFactory _indexerFactory;
|
private readonly IIndexerFactory _indexerFactory;
|
||||||
|
private readonly IDownloadClientFactory _downloadClientFactory;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
private static readonly object PushLock = new object();
|
private static readonly object PushLock = new object();
|
||||||
|
@ -28,6 +29,7 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker,
|
public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker,
|
||||||
IProcessDownloadDecisions downloadDecisionProcessor,
|
IProcessDownloadDecisions downloadDecisionProcessor,
|
||||||
IIndexerFactory indexerFactory,
|
IIndexerFactory indexerFactory,
|
||||||
|
IDownloadClientFactory downloadClientFactory,
|
||||||
IQualityProfileService qualityProfileService,
|
IQualityProfileService qualityProfileService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(qualityProfileService)
|
: base(qualityProfileService)
|
||||||
|
@ -35,6 +37,7 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
_downloadDecisionMaker = downloadDecisionMaker;
|
_downloadDecisionMaker = downloadDecisionMaker;
|
||||||
_downloadDecisionProcessor = downloadDecisionProcessor;
|
_downloadDecisionProcessor = downloadDecisionProcessor;
|
||||||
_indexerFactory = indexerFactory;
|
_indexerFactory = indexerFactory;
|
||||||
|
_downloadClientFactory = downloadClientFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
||||||
|
@ -44,6 +47,7 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[Consumes("application/json")]
|
||||||
public ActionResult<ReleaseResource> Create(ReleaseResource release)
|
public ActionResult<ReleaseResource> Create(ReleaseResource release)
|
||||||
{
|
{
|
||||||
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl);
|
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl);
|
||||||
|
@ -56,22 +60,25 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
|
|
||||||
ResolveIndexer(info);
|
ResolveIndexer(info);
|
||||||
|
|
||||||
List<DownloadDecision> decisions;
|
var downloadClientId = ResolveDownloadClientId(release);
|
||||||
|
|
||||||
|
DownloadDecision decision;
|
||||||
|
|
||||||
lock (PushLock)
|
lock (PushLock)
|
||||||
{
|
{
|
||||||
decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info });
|
var decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info }, true);
|
||||||
_downloadDecisionProcessor.ProcessDecisions(decisions).GetAwaiter().GetResult();
|
|
||||||
|
decision = decisions.FirstOrDefault();
|
||||||
|
|
||||||
|
_downloadDecisionProcessor.ProcessDecision(decision, downloadClientId).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstDecision = decisions.FirstOrDefault();
|
if (decision?.RemoteAlbum.ParsedAlbumInfo == null)
|
||||||
|
|
||||||
if (firstDecision?.RemoteAlbum.ParsedAlbumInfo == null)
|
|
||||||
{
|
{
|
||||||
throw new ValidationException(new List<ValidationFailure> { new ValidationFailure("Title", "Unable to parse", release.Title) });
|
throw new ValidationException(new List<ValidationFailure> { new ("Title", "Unable to parse", release.Title) });
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapDecisions(new[] { firstDecision }).First();
|
return MapDecisions(new[] { decision }).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResolveIndexer(ReleaseInfo release)
|
private void ResolveIndexer(ReleaseInfo release)
|
||||||
|
@ -86,7 +93,7 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.Debug("Push Release {0} not associated with unknown indexer {1}.", release.Title, release.Indexer);
|
_logger.Debug("Push Release {0} not associated with known indexer {1}.", release.Title, release.Indexer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (release.IndexerId != 0 && release.Indexer.IsNullOrWhiteSpace())
|
else if (release.IndexerId != 0 && release.Indexer.IsNullOrWhiteSpace())
|
||||||
|
@ -99,7 +106,7 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
}
|
}
|
||||||
catch (ModelNotFoundException)
|
catch (ModelNotFoundException)
|
||||||
{
|
{
|
||||||
_logger.Debug("Push Release {0} not associated with unknown indexer {1}.", release.Title, release.IndexerId);
|
_logger.Debug("Push Release {0} not associated with known indexer {1}.", release.Title, release.IndexerId);
|
||||||
release.IndexerId = 0;
|
release.IndexerId = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,5 +115,26 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
_logger.Debug("Push Release {0} not associated with an indexer.", release.Title);
|
_logger.Debug("Push Release {0} not associated with an indexer.", release.Title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int? ResolveDownloadClientId(ReleaseResource release)
|
||||||
|
{
|
||||||
|
var downloadClientId = release.DownloadClientId.GetValueOrDefault();
|
||||||
|
|
||||||
|
if (downloadClientId == 0 && release.DownloadClient.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
var downloadClient = _downloadClientFactory.All().FirstOrDefault(v => v.Name.EqualsIgnoreCase(release.DownloadClient));
|
||||||
|
|
||||||
|
if (downloadClient != null)
|
||||||
|
{
|
||||||
|
_logger.Debug("Push Release {0} associated with download client {1} - {2}.", release.Title, downloadClientId, release.DownloadClient);
|
||||||
|
|
||||||
|
return downloadClient.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Push Release {0} not associated with known download client {1}.", release.Title, release.DownloadClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
return release.DownloadClientId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,12 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
|
|
||||||
// [JsonIgnore]
|
// [JsonIgnore]
|
||||||
public int? AlbumId { get; set; }
|
public int? AlbumId { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
public int? DownloadClientId { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
public string DownloadClient { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ReleaseResourceMapper
|
public static class ReleaseResourceMapper
|
||||||
|
|
|
@ -9,8 +9,9 @@ namespace Lidarr.Api.V1.Notifications
|
||||||
public bool OnReleaseImport { get; set; }
|
public bool OnReleaseImport { get; set; }
|
||||||
public bool OnUpgrade { get; set; }
|
public bool OnUpgrade { get; set; }
|
||||||
public bool OnRename { get; set; }
|
public bool OnRename { get; set; }
|
||||||
public bool OnAlbumDelete { get; set; }
|
public bool OnArtistAdd { get; set; }
|
||||||
public bool OnArtistDelete { get; set; }
|
public bool OnArtistDelete { get; set; }
|
||||||
|
public bool OnAlbumDelete { get; set; }
|
||||||
public bool OnHealthIssue { get; set; }
|
public bool OnHealthIssue { get; set; }
|
||||||
public bool OnHealthRestored { get; set; }
|
public bool OnHealthRestored { get; set; }
|
||||||
public bool OnDownloadFailure { get; set; }
|
public bool OnDownloadFailure { get; set; }
|
||||||
|
@ -21,8 +22,9 @@ namespace Lidarr.Api.V1.Notifications
|
||||||
public bool SupportsOnReleaseImport { get; set; }
|
public bool SupportsOnReleaseImport { get; set; }
|
||||||
public bool SupportsOnUpgrade { get; set; }
|
public bool SupportsOnUpgrade { get; set; }
|
||||||
public bool SupportsOnRename { get; set; }
|
public bool SupportsOnRename { get; set; }
|
||||||
public bool SupportsOnAlbumDelete { get; set; }
|
public bool SupportsOnArtistAdd { get; set; }
|
||||||
public bool SupportsOnArtistDelete { get; set; }
|
public bool SupportsOnArtistDelete { get; set; }
|
||||||
|
public bool SupportsOnAlbumDelete { get; set; }
|
||||||
public bool SupportsOnHealthIssue { get; set; }
|
public bool SupportsOnHealthIssue { get; set; }
|
||||||
public bool SupportsOnHealthRestored { get; set; }
|
public bool SupportsOnHealthRestored { get; set; }
|
||||||
public bool IncludeHealthWarnings { get; set; }
|
public bool IncludeHealthWarnings { get; set; }
|
||||||
|
@ -48,8 +50,9 @@ namespace Lidarr.Api.V1.Notifications
|
||||||
resource.OnReleaseImport = definition.OnReleaseImport;
|
resource.OnReleaseImport = definition.OnReleaseImport;
|
||||||
resource.OnUpgrade = definition.OnUpgrade;
|
resource.OnUpgrade = definition.OnUpgrade;
|
||||||
resource.OnRename = definition.OnRename;
|
resource.OnRename = definition.OnRename;
|
||||||
resource.OnAlbumDelete = definition.OnAlbumDelete;
|
resource.OnArtistAdd = definition.OnArtistAdd;
|
||||||
resource.OnArtistDelete = definition.OnArtistDelete;
|
resource.OnArtistDelete = definition.OnArtistDelete;
|
||||||
|
resource.OnAlbumDelete = definition.OnAlbumDelete;
|
||||||
resource.OnHealthIssue = definition.OnHealthIssue;
|
resource.OnHealthIssue = definition.OnHealthIssue;
|
||||||
resource.OnHealthRestored = definition.OnHealthRestored;
|
resource.OnHealthRestored = definition.OnHealthRestored;
|
||||||
resource.OnDownloadFailure = definition.OnDownloadFailure;
|
resource.OnDownloadFailure = definition.OnDownloadFailure;
|
||||||
|
@ -60,8 +63,9 @@ namespace Lidarr.Api.V1.Notifications
|
||||||
resource.SupportsOnReleaseImport = definition.SupportsOnReleaseImport;
|
resource.SupportsOnReleaseImport = definition.SupportsOnReleaseImport;
|
||||||
resource.SupportsOnUpgrade = definition.SupportsOnUpgrade;
|
resource.SupportsOnUpgrade = definition.SupportsOnUpgrade;
|
||||||
resource.SupportsOnRename = definition.SupportsOnRename;
|
resource.SupportsOnRename = definition.SupportsOnRename;
|
||||||
resource.SupportsOnAlbumDelete = definition.SupportsOnAlbumDelete;
|
resource.SupportsOnArtistAdd = definition.SupportsOnArtistAdd;
|
||||||
resource.SupportsOnArtistDelete = definition.SupportsOnArtistDelete;
|
resource.SupportsOnArtistDelete = definition.SupportsOnArtistDelete;
|
||||||
|
resource.SupportsOnAlbumDelete = definition.SupportsOnAlbumDelete;
|
||||||
resource.SupportsOnHealthIssue = definition.SupportsOnHealthIssue;
|
resource.SupportsOnHealthIssue = definition.SupportsOnHealthIssue;
|
||||||
resource.SupportsOnHealthRestored = definition.SupportsOnHealthRestored;
|
resource.SupportsOnHealthRestored = definition.SupportsOnHealthRestored;
|
||||||
resource.IncludeHealthWarnings = definition.IncludeHealthWarnings;
|
resource.IncludeHealthWarnings = definition.IncludeHealthWarnings;
|
||||||
|
@ -86,8 +90,9 @@ namespace Lidarr.Api.V1.Notifications
|
||||||
definition.OnReleaseImport = resource.OnReleaseImport;
|
definition.OnReleaseImport = resource.OnReleaseImport;
|
||||||
definition.OnUpgrade = resource.OnUpgrade;
|
definition.OnUpgrade = resource.OnUpgrade;
|
||||||
definition.OnRename = resource.OnRename;
|
definition.OnRename = resource.OnRename;
|
||||||
definition.OnAlbumDelete = resource.OnAlbumDelete;
|
definition.OnArtistAdd = resource.OnArtistAdd;
|
||||||
definition.OnArtistDelete = resource.OnArtistDelete;
|
definition.OnArtistDelete = resource.OnArtistDelete;
|
||||||
|
definition.OnAlbumDelete = resource.OnAlbumDelete;
|
||||||
definition.OnHealthIssue = resource.OnHealthIssue;
|
definition.OnHealthIssue = resource.OnHealthIssue;
|
||||||
definition.OnHealthRestored = resource.OnHealthRestored;
|
definition.OnHealthRestored = resource.OnHealthRestored;
|
||||||
definition.OnDownloadFailure = resource.OnDownloadFailure;
|
definition.OnDownloadFailure = resource.OnDownloadFailure;
|
||||||
|
@ -98,8 +103,9 @@ namespace Lidarr.Api.V1.Notifications
|
||||||
definition.SupportsOnReleaseImport = resource.SupportsOnReleaseImport;
|
definition.SupportsOnReleaseImport = resource.SupportsOnReleaseImport;
|
||||||
definition.SupportsOnUpgrade = resource.SupportsOnUpgrade;
|
definition.SupportsOnUpgrade = resource.SupportsOnUpgrade;
|
||||||
definition.SupportsOnRename = resource.SupportsOnRename;
|
definition.SupportsOnRename = resource.SupportsOnRename;
|
||||||
definition.SupportsOnAlbumDelete = resource.SupportsOnAlbumDelete;
|
definition.SupportsOnArtistAdd = resource.SupportsOnArtistAdd;
|
||||||
definition.SupportsOnArtistDelete = resource.SupportsOnArtistDelete;
|
definition.SupportsOnArtistDelete = resource.SupportsOnArtistDelete;
|
||||||
|
definition.SupportsOnAlbumDelete = resource.SupportsOnAlbumDelete;
|
||||||
definition.SupportsOnHealthIssue = resource.SupportsOnHealthIssue;
|
definition.SupportsOnHealthIssue = resource.SupportsOnHealthIssue;
|
||||||
definition.SupportsOnHealthRestored = resource.SupportsOnHealthRestored;
|
definition.SupportsOnHealthRestored = resource.SupportsOnHealthRestored;
|
||||||
definition.IncludeHealthWarnings = resource.IncludeHealthWarnings;
|
definition.IncludeHealthWarnings = resource.IncludeHealthWarnings;
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
using Lidarr.Api.V1.Albums;
|
using Lidarr.Api.V1.Albums;
|
||||||
using Lidarr.Api.V1.Artist;
|
using Lidarr.Api.V1.Artist;
|
||||||
|
using Lidarr.Api.V1.CustomFormats;
|
||||||
using Lidarr.Http;
|
using Lidarr.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
|
using NzbDrone.Core.Download.Aggregation;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
|
|
||||||
namespace Lidarr.Api.V1.Parse
|
namespace Lidarr.Api.V1.Parse
|
||||||
|
@ -10,16 +14,27 @@ namespace Lidarr.Api.V1.Parse
|
||||||
public class ParseController : Controller
|
public class ParseController : Controller
|
||||||
{
|
{
|
||||||
private readonly IParsingService _parsingService;
|
private readonly IParsingService _parsingService;
|
||||||
|
private readonly IRemoteAlbumAggregationService _aggregationService;
|
||||||
|
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||||
|
|
||||||
public ParseController(IParsingService parsingService)
|
public ParseController(IParsingService parsingService,
|
||||||
|
IRemoteAlbumAggregationService aggregationService,
|
||||||
|
ICustomFormatCalculationService formatCalculator)
|
||||||
{
|
{
|
||||||
_parsingService = parsingService;
|
_parsingService = parsingService;
|
||||||
|
_aggregationService = aggregationService;
|
||||||
|
_formatCalculator = formatCalculator;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public ParseResource Parse(string title)
|
public ParseResource Parse(string title)
|
||||||
{
|
{
|
||||||
|
if (title.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var parsedAlbumInfo = Parser.ParseAlbumTitle(title);
|
var parsedAlbumInfo = Parser.ParseAlbumTitle(title);
|
||||||
|
|
||||||
if (parsedAlbumInfo == null)
|
if (parsedAlbumInfo == null)
|
||||||
|
@ -34,12 +49,19 @@ namespace Lidarr.Api.V1.Parse
|
||||||
|
|
||||||
if (remoteAlbum != null)
|
if (remoteAlbum != null)
|
||||||
{
|
{
|
||||||
|
_aggregationService.Augment(remoteAlbum);
|
||||||
|
|
||||||
|
remoteAlbum.CustomFormats = _formatCalculator.ParseCustomFormat(remoteAlbum, 0);
|
||||||
|
remoteAlbum.CustomFormatScore = remoteAlbum?.Artist?.QualityProfile?.Value.CalculateCustomFormatScore(remoteAlbum.CustomFormats) ?? 0;
|
||||||
|
|
||||||
return new ParseResource
|
return new ParseResource
|
||||||
{
|
{
|
||||||
Title = title,
|
Title = title,
|
||||||
ParsedAlbumInfo = remoteAlbum.ParsedAlbumInfo,
|
ParsedAlbumInfo = remoteAlbum.ParsedAlbumInfo,
|
||||||
Artist = remoteAlbum.Artist.ToResource(),
|
Artist = remoteAlbum.Artist.ToResource(),
|
||||||
Albums = remoteAlbum.Albums.ToResource()
|
Albums = remoteAlbum.Albums.ToResource(),
|
||||||
|
CustomFormats = remoteAlbum.CustomFormats?.ToResource(false),
|
||||||
|
CustomFormatScore = remoteAlbum.CustomFormatScore
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Lidarr.Api.V1.Albums;
|
using Lidarr.Api.V1.Albums;
|
||||||
using Lidarr.Api.V1.Artist;
|
using Lidarr.Api.V1.Artist;
|
||||||
|
using Lidarr.Api.V1.CustomFormats;
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
@ -12,5 +13,7 @@ namespace Lidarr.Api.V1.Parse
|
||||||
public ParsedAlbumInfo ParsedAlbumInfo { get; set; }
|
public ParsedAlbumInfo ParsedAlbumInfo { get; set; }
|
||||||
public ArtistResource Artist { get; set; }
|
public ArtistResource Artist { get; set; }
|
||||||
public List<AlbumResource> Albums { get; set; }
|
public List<AlbumResource> Albums { get; set; }
|
||||||
|
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||||
|
public int CustomFormatScore { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ namespace Lidarr.Api.V1.Qualities
|
||||||
|
|
||||||
public double? MinSize { get; set; }
|
public double? MinSize { get; set; }
|
||||||
public double? MaxSize { get; set; }
|
public double? MaxSize { get; set; }
|
||||||
|
public double? PreferredSize { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class QualityDefinitionResourceMapper
|
public static class QualityDefinitionResourceMapper
|
||||||
|
@ -33,7 +34,8 @@ namespace Lidarr.Api.V1.Qualities
|
||||||
Title = model.Title,
|
Title = model.Title,
|
||||||
Weight = model.Weight,
|
Weight = model.Weight,
|
||||||
MinSize = model.MinSize,
|
MinSize = model.MinSize,
|
||||||
MaxSize = model.MaxSize
|
MaxSize = model.MaxSize,
|
||||||
|
PreferredSize = model.PreferredSize
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +53,8 @@ namespace Lidarr.Api.V1.Qualities
|
||||||
Title = resource.Title,
|
Title = resource.Title,
|
||||||
Weight = resource.Weight,
|
Weight = resource.Weight,
|
||||||
MinSize = resource.MinSize,
|
MinSize = resource.MinSize,
|
||||||
MaxSize = resource.MaxSize
|
MaxSize = resource.MaxSize,
|
||||||
|
PreferredSize = resource.PreferredSize
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ namespace Lidarr.Api.V1.Queue
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _downloadService.DownloadReport(pendingRelease.RemoteAlbum);
|
await _downloadService.DownloadReport(pendingRelease.RemoteAlbum, null);
|
||||||
|
|
||||||
return new { };
|
return new { };
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ namespace Lidarr.Api.V1.Queue
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _downloadService.DownloadReport(pendingRelease.RemoteAlbum);
|
await _downloadService.DownloadReport(pendingRelease.RemoteAlbum, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new { };
|
return new { };
|
||||||
|
|
|
@ -65,7 +65,7 @@ namespace Lidarr.Api.V1.Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
[RestDeleteById]
|
[RestDeleteById]
|
||||||
public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false)
|
public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false, bool changeCategory = false)
|
||||||
{
|
{
|
||||||
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
|
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
|
||||||
|
|
||||||
|
@ -83,12 +83,12 @@ namespace Lidarr.Api.V1.Queue
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload);
|
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory);
|
||||||
_trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId);
|
_trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("bulk")]
|
[HttpDelete("bulk")]
|
||||||
public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false)
|
public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false, [FromQuery] bool changeCategory = false)
|
||||||
{
|
{
|
||||||
var trackedDownloadIds = new List<string>();
|
var trackedDownloadIds = new List<string>();
|
||||||
var pendingToRemove = new List<NzbDrone.Core.Queue.Queue>();
|
var pendingToRemove = new List<NzbDrone.Core.Queue.Queue>();
|
||||||
|
@ -119,7 +119,7 @@ namespace Lidarr.Api.V1.Queue
|
||||||
|
|
||||||
foreach (var trackedDownload in trackedToRemove.DistinctBy(t => t.DownloadItem.DownloadId))
|
foreach (var trackedDownload in trackedToRemove.DistinctBy(t => t.DownloadItem.DownloadId))
|
||||||
{
|
{
|
||||||
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload);
|
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory);
|
||||||
trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId);
|
trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,9 +181,16 @@ namespace Lidarr.Api.V1.Queue
|
||||||
else if (pagingSpec.SortKey == "estimatedCompletionTime")
|
else if (pagingSpec.SortKey == "estimatedCompletionTime")
|
||||||
{
|
{
|
||||||
ordered = ascending
|
ordered = ascending
|
||||||
? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer())
|
? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new DatetimeComparer())
|
||||||
: fullQueue.OrderByDescending(q => q.EstimatedCompletionTime,
|
: fullQueue.OrderByDescending(q => q.EstimatedCompletionTime,
|
||||||
new EstimatedCompletionTimeComparer());
|
new DatetimeComparer());
|
||||||
|
}
|
||||||
|
else if (pagingSpec.SortKey == "added")
|
||||||
|
{
|
||||||
|
ordered = ascending
|
||||||
|
? fullQueue.OrderBy(q => q.Added, new DatetimeComparer())
|
||||||
|
: fullQueue.OrderByDescending(q => q.Added,
|
||||||
|
new DatetimeComparer());
|
||||||
}
|
}
|
||||||
else if (pagingSpec.SortKey == "protocol")
|
else if (pagingSpec.SortKey == "protocol")
|
||||||
{
|
{
|
||||||
|
@ -262,7 +269,7 @@ namespace Lidarr.Api.V1.Queue
|
||||||
_pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id);
|
_pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TrackedDownload Remove(TrackedDownload trackedDownload, bool removeFromClient, bool blocklist, bool skipRedownload)
|
private TrackedDownload Remove(TrackedDownload trackedDownload, bool removeFromClient, bool blocklist, bool skipRedownload, bool changeCategory)
|
||||||
{
|
{
|
||||||
if (removeFromClient)
|
if (removeFromClient)
|
||||||
{
|
{
|
||||||
|
@ -275,13 +282,24 @@ namespace Lidarr.Api.V1.Queue
|
||||||
|
|
||||||
downloadClient.RemoveItem(trackedDownload.DownloadItem, true);
|
downloadClient.RemoveItem(trackedDownload.DownloadItem, true);
|
||||||
}
|
}
|
||||||
|
else if (changeCategory)
|
||||||
|
{
|
||||||
|
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
|
||||||
|
|
||||||
|
if (downloadClient == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadClient.MarkItemAsImported(trackedDownload.DownloadItem);
|
||||||
|
}
|
||||||
|
|
||||||
if (blocklist)
|
if (blocklist)
|
||||||
{
|
{
|
||||||
_failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipRedownload);
|
_failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipRedownload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!removeFromClient && !blocklist)
|
if (!removeFromClient && !blocklist && !changeCategory)
|
||||||
{
|
{
|
||||||
if (!_ignoredDownloadService.IgnoreDownload(trackedDownload))
|
if (!_ignoredDownloadService.IgnoreDownload(trackedDownload))
|
||||||
{
|
{
|
||||||
|
|
|
@ -26,6 +26,7 @@ namespace Lidarr.Api.V1.Queue
|
||||||
public decimal Sizeleft { get; set; }
|
public decimal Sizeleft { get; set; }
|
||||||
public TimeSpan? Timeleft { get; set; }
|
public TimeSpan? Timeleft { get; set; }
|
||||||
public DateTime? EstimatedCompletionTime { get; set; }
|
public DateTime? EstimatedCompletionTime { get; set; }
|
||||||
|
public DateTime? Added { get; set; }
|
||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
public TrackedDownloadStatus? TrackedDownloadStatus { get; set; }
|
public TrackedDownloadStatus? TrackedDownloadStatus { get; set; }
|
||||||
public TrackedDownloadState? TrackedDownloadState { get; set; }
|
public TrackedDownloadState? TrackedDownloadState { get; set; }
|
||||||
|
@ -34,6 +35,7 @@ namespace Lidarr.Api.V1.Queue
|
||||||
public string DownloadId { get; set; }
|
public string DownloadId { get; set; }
|
||||||
public DownloadProtocol Protocol { get; set; }
|
public DownloadProtocol Protocol { get; set; }
|
||||||
public string DownloadClient { get; set; }
|
public string DownloadClient { get; set; }
|
||||||
|
public bool DownloadClientHasPostImportCategory { get; set; }
|
||||||
public string Indexer { get; set; }
|
public string Indexer { get; set; }
|
||||||
public string OutputPath { get; set; }
|
public string OutputPath { get; set; }
|
||||||
public bool DownloadForced { get; set; }
|
public bool DownloadForced { get; set; }
|
||||||
|
@ -66,6 +68,7 @@ namespace Lidarr.Api.V1.Queue
|
||||||
Sizeleft = model.Sizeleft,
|
Sizeleft = model.Sizeleft,
|
||||||
Timeleft = model.Timeleft,
|
Timeleft = model.Timeleft,
|
||||||
EstimatedCompletionTime = model.EstimatedCompletionTime,
|
EstimatedCompletionTime = model.EstimatedCompletionTime,
|
||||||
|
Added = model.Added,
|
||||||
Status = model.Status.FirstCharToLower(),
|
Status = model.Status.FirstCharToLower(),
|
||||||
TrackedDownloadStatus = model.TrackedDownloadStatus,
|
TrackedDownloadStatus = model.TrackedDownloadStatus,
|
||||||
TrackedDownloadState = model.TrackedDownloadState,
|
TrackedDownloadState = model.TrackedDownloadState,
|
||||||
|
@ -74,6 +77,7 @@ namespace Lidarr.Api.V1.Queue
|
||||||
DownloadId = model.DownloadId,
|
DownloadId = model.DownloadId,
|
||||||
Protocol = model.Protocol,
|
Protocol = model.Protocol,
|
||||||
DownloadClient = model.DownloadClient,
|
DownloadClient = model.DownloadClient,
|
||||||
|
DownloadClientHasPostImportCategory = model.DownloadClientHasPostImportCategory,
|
||||||
Indexer = model.Indexer,
|
Indexer = model.Indexer,
|
||||||
OutputPath = model.OutputPath,
|
OutputPath = model.OutputPath,
|
||||||
DownloadForced = model.DownloadForced
|
DownloadForced = model.DownloadForced
|
||||||
|
|
|
@ -90,7 +90,7 @@ namespace Lidarr.Api.V1.System.Backup
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("restore/upload")]
|
[HttpPost("restore/upload")]
|
||||||
[RequestFormLimits(MultipartBodyLengthLimit = 500000000)]
|
[RequestFormLimits(MultipartBodyLengthLimit = 1000000000)]
|
||||||
public object UploadAndRestore()
|
public object UploadAndRestore()
|
||||||
{
|
{
|
||||||
var files = Request.Form.Files;
|
var files = Request.Form.Files;
|
||||||
|
|
|
@ -2638,9 +2638,12 @@
|
||||||
"name": "eventType",
|
"name": "eventType",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "albumId",
|
"name": "albumId",
|
||||||
|
@ -6031,6 +6034,14 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "changeCategory",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -6069,6 +6080,14 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "changeCategory",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
|
@ -6636,16 +6655,6 @@
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/ReleaseResource"
|
"$ref": "#/components/schemas/ReleaseResource"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"text/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ReleaseResource"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/*+json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ReleaseResource"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -9407,6 +9416,9 @@
|
||||||
},
|
},
|
||||||
"autoRedownloadFailed": {
|
"autoRedownloadFailed": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"autoRedownloadFailedFromInteractiveSearch": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -11100,6 +11112,17 @@
|
||||||
"$ref": "#/components/schemas/AlbumResource"
|
"$ref": "#/components/schemas/AlbumResource"
|
||||||
},
|
},
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"customFormats": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/CustomFormatResource"
|
||||||
|
},
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"customFormatScore": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -11437,6 +11460,11 @@
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "double",
|
"format": "double",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"preferredSize": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "double",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -11594,6 +11622,11 @@
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"added": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
@ -11626,6 +11659,9 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"downloadClientHasPostImportCategory": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"indexer": {
|
"indexer": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
@ -11936,6 +11972,15 @@
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"downloadClientId": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"downloadClient": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
|
@ -3,11 +3,13 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DryIoc;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Reflection;
|
using NzbDrone.Common.Reflection;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
|
|
||||||
namespace Lidarr.Http.ClientSchema
|
namespace Lidarr.Http.ClientSchema
|
||||||
{
|
{
|
||||||
|
@ -15,6 +17,12 @@ namespace Lidarr.Http.ClientSchema
|
||||||
{
|
{
|
||||||
private const string PRIVATE_VALUE = "********";
|
private const string PRIVATE_VALUE = "********";
|
||||||
private static Dictionary<Type, FieldMapping[]> _mappings = new Dictionary<Type, FieldMapping[]>();
|
private static Dictionary<Type, FieldMapping[]> _mappings = new Dictionary<Type, FieldMapping[]>();
|
||||||
|
private static ILocalizationService _localizationService;
|
||||||
|
|
||||||
|
public static void Initialize(IContainer container)
|
||||||
|
{
|
||||||
|
_localizationService = container.Resolve<ILocalizationService>();
|
||||||
|
}
|
||||||
|
|
||||||
public static List<Field> ToSchema(object model)
|
public static List<Field> ToSchema(object model)
|
||||||
{
|
{
|
||||||
|
@ -106,13 +114,27 @@ namespace Lidarr.Http.ClientSchema
|
||||||
if (propertyInfo.PropertyType.IsSimpleType())
|
if (propertyInfo.PropertyType.IsSimpleType())
|
||||||
{
|
{
|
||||||
var fieldAttribute = property.Item2;
|
var fieldAttribute = property.Item2;
|
||||||
|
|
||||||
|
var label = fieldAttribute.Label.IsNotNullOrWhiteSpace()
|
||||||
|
? _localizationService.GetLocalizedString(fieldAttribute.Label,
|
||||||
|
GetTokens(type, fieldAttribute.Label, TokenField.Label))
|
||||||
|
: fieldAttribute.Label;
|
||||||
|
var helpText = fieldAttribute.HelpText.IsNotNullOrWhiteSpace()
|
||||||
|
? _localizationService.GetLocalizedString(fieldAttribute.HelpText,
|
||||||
|
GetTokens(type, fieldAttribute.Label, TokenField.HelpText))
|
||||||
|
: fieldAttribute.HelpText;
|
||||||
|
var helpTextWarning = fieldAttribute.HelpTextWarning.IsNotNullOrWhiteSpace()
|
||||||
|
? _localizationService.GetLocalizedString(fieldAttribute.HelpTextWarning,
|
||||||
|
GetTokens(type, fieldAttribute.Label, TokenField.HelpTextWarning))
|
||||||
|
: fieldAttribute.HelpTextWarning;
|
||||||
|
|
||||||
var field = new Field
|
var field = new Field
|
||||||
{
|
{
|
||||||
Name = prefix + GetCamelCaseName(propertyInfo.Name),
|
Name = prefix + GetCamelCaseName(propertyInfo.Name),
|
||||||
Label = fieldAttribute.Label,
|
Label = label,
|
||||||
Unit = fieldAttribute.Unit,
|
Unit = fieldAttribute.Unit,
|
||||||
HelpText = fieldAttribute.HelpText,
|
HelpText = helpText,
|
||||||
HelpTextWarning = fieldAttribute.HelpTextWarning,
|
HelpTextWarning = helpTextWarning,
|
||||||
HelpLink = fieldAttribute.HelpLink,
|
HelpLink = fieldAttribute.HelpLink,
|
||||||
Order = fieldAttribute.Order,
|
Order = fieldAttribute.Order,
|
||||||
Advanced = fieldAttribute.Advanced,
|
Advanced = fieldAttribute.Advanced,
|
||||||
|
@ -172,6 +194,24 @@ namespace Lidarr.Http.ClientSchema
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, object> GetTokens(Type type, string label, TokenField field)
|
||||||
|
{
|
||||||
|
var tokens = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
foreach (var propertyInfo in type.GetProperties())
|
||||||
|
{
|
||||||
|
foreach (var attribute in propertyInfo.GetCustomAttributes(true))
|
||||||
|
{
|
||||||
|
if (attribute is FieldTokenAttribute fieldTokenAttribute && fieldTokenAttribute.Field == field && fieldTokenAttribute.Label == label)
|
||||||
|
{
|
||||||
|
tokens.Add(fieldTokenAttribute.Token, fieldTokenAttribute.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<SelectOption> GetSelectOptions(Type selectOptions)
|
private static List<SelectOption> GetSelectOptions(Type selectOptions)
|
||||||
{
|
{
|
||||||
if (selectOptions.IsEnum)
|
if (selectOptions.IsEnum)
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Lidarr.Http.ClientSchema;
|
using Lidarr.Http.ClientSchema;
|
||||||
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
namespace NzbDrone.Api.Test.ClientSchemaTests
|
namespace NzbDrone.Api.Test.ClientSchemaTests
|
||||||
|
@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class SchemaBuilderFixture : TestBase
|
public class SchemaBuilderFixture : TestBase
|
||||||
{
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<ILocalizationService>()
|
||||||
|
.Setup(s => s.GetLocalizedString(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>()))
|
||||||
|
.Returns<string, Dictionary<string, object>>((s, d) => s);
|
||||||
|
|
||||||
|
SchemaBuilder.Initialize(Mocker.Container);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_field_for_every_property()
|
public void should_return_field_for_every_property()
|
||||||
{
|
{
|
||||||
|
|
|
@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
data = new XElement("base64", Convert.ToBase64String(bytes));
|
data = new XElement("base64", Convert.ToBase64String(bytes));
|
||||||
}
|
}
|
||||||
|
else if (value is Dictionary<string, string> d)
|
||||||
|
{
|
||||||
|
data = new XElement("struct", d.Select(p => new XElement("member", new XElement("name", p.Key), new XElement("value", p.Value))));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
|
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NLog" Version="5.2.0" />
|
<PackageReference Include="NLog" Version="5.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||||
<PackageReference Include="Sentry" Version="3.25.0" />
|
<PackageReference Include="Sentry" Version="3.25.0" />
|
||||||
|
|
|
@ -25,12 +25,30 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
|
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
|
||||||
|
|
||||||
|
Mocker.GetMock<IQualityDefinitionService>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<Quality>()))
|
||||||
|
.Returns(new QualityDefinition { PreferredSize = null });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenPreferredSize(double? size)
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IQualityDefinitionService>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<Quality>()))
|
||||||
|
.Returns(new QualityDefinition { PreferredSize = size });
|
||||||
}
|
}
|
||||||
|
|
||||||
private Album GivenAlbum(int id)
|
private Album GivenAlbum(int id)
|
||||||
{
|
{
|
||||||
|
var release = Builder<AlbumRelease>.CreateNew()
|
||||||
|
.With(e => e.AlbumId = id)
|
||||||
|
.With(e => e.Monitored = true)
|
||||||
|
.With(e => e.Duration = 3600000)
|
||||||
|
.Build();
|
||||||
|
|
||||||
return Builder<Album>.CreateNew()
|
return Builder<Album>.CreateNew()
|
||||||
.With(e => e.Id = id)
|
.With(e => e.Id = id)
|
||||||
|
.With(e => e.AlbumReleases = new List<AlbumRelease> { release })
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +148,44 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
qualifiedReports.First().RemoteAlbum.Should().Be(remoteAlbumHdLargeYoung);
|
qualifiedReports.First().RemoteAlbum.Should().Be(remoteAlbumHdLargeYoung);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_order_by_closest_to_preferred_size_if_both_under()
|
||||||
|
{
|
||||||
|
// 1000 Kibit/Sec * 60 Min Duration = 439.5 MiB
|
||||||
|
GivenPreferredSize(1000);
|
||||||
|
|
||||||
|
var remoteAlbumSmall = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 120.Megabytes(), age: 1);
|
||||||
|
var remoteAlbumLarge = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 400.Megabytes(), age: 1);
|
||||||
|
|
||||||
|
var decisions = new List<DownloadDecision>();
|
||||||
|
decisions.Add(new DownloadDecision(remoteAlbumSmall));
|
||||||
|
decisions.Add(new DownloadDecision(remoteAlbumLarge));
|
||||||
|
|
||||||
|
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
|
||||||
|
qualifiedReports.First().RemoteAlbum.Should().Be(remoteAlbumLarge);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_order_by_closest_to_preferred_size_if_preferred_is_in_between()
|
||||||
|
{
|
||||||
|
// 700 Kibit/Sec * 60 Min Duration = 307.6 MiB
|
||||||
|
GivenPreferredSize(700);
|
||||||
|
|
||||||
|
var remoteAlbum1 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 100.Megabytes(), age: 1);
|
||||||
|
var remoteAlbum2 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 200.Megabytes(), age: 1);
|
||||||
|
var remoteAlbum3 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 300.Megabytes(), age: 1);
|
||||||
|
var remoteAlbum4 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 500.Megabytes(), age: 1);
|
||||||
|
|
||||||
|
var decisions = new List<DownloadDecision>();
|
||||||
|
decisions.Add(new DownloadDecision(remoteAlbum1));
|
||||||
|
decisions.Add(new DownloadDecision(remoteAlbum2));
|
||||||
|
decisions.Add(new DownloadDecision(remoteAlbum3));
|
||||||
|
decisions.Add(new DownloadDecision(remoteAlbum4));
|
||||||
|
|
||||||
|
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
|
||||||
|
qualifiedReports.First().RemoteAlbum.Should().Be(remoteAlbum3);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_order_by_youngest()
|
public void should_order_by_youngest()
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,6 +4,7 @@ using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.Download.TrackedDownloads;
|
using NzbDrone.Core.Download.TrackedDownloads;
|
||||||
|
@ -369,5 +370,31 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_same_quality_non_proper_in_queue_and_download_propers_is_do_not_upgrade()
|
||||||
|
{
|
||||||
|
_remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_008, new Revision(2));
|
||||||
|
_artist.QualityProfile.Value.Cutoff = _remoteAlbum.ParsedAlbumInfo.Quality.Quality.Id;
|
||||||
|
|
||||||
|
Mocker.GetMock<IConfigService>()
|
||||||
|
.Setup(s => s.DownloadPropersAndRepacks)
|
||||||
|
.Returns(ProperDownloadTypes.DoNotUpgrade);
|
||||||
|
|
||||||
|
var remoteAlbum = Builder<RemoteAlbum>.CreateNew()
|
||||||
|
.With(r => r.Artist = _artist)
|
||||||
|
.With(r => r.Albums = new List<Album> { _album })
|
||||||
|
.With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo
|
||||||
|
{
|
||||||
|
Quality = new QualityModel(Quality.MP3_008)
|
||||||
|
})
|
||||||
|
.With(r => r.Release = _releaseInfo)
|
||||||
|
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
GivenQueue(new List<RemoteAlbum> { remoteAlbum });
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum));
|
decisions.Add(new DownloadDecision(remoteAlbum));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum));
|
decisions.Add(new DownloadDecision(remoteAlbum));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -101,7 +101,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum2));
|
decisions.Add(new DownloadDecision(remoteAlbum2));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -172,7 +172,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||||
var decisions = new List<DownloadDecision>();
|
var decisions = new List<DownloadDecision>();
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum));
|
decisions.Add(new DownloadDecision(remoteAlbum));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteAlbum>())).Throws(new Exception());
|
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteAlbum>(), null)).Throws(new Exception());
|
||||||
|
|
||||||
var result = await Subject.ProcessDecisions(decisions);
|
var result = await Subject.ProcessDecisions(decisions);
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary)));
|
decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary)));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>()), Times.Never());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>(), null), Times.Never());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -242,11 +242,11 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum));
|
decisions.Add(new DownloadDecision(remoteAlbum));
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum));
|
decisions.Add(new DownloadDecision(remoteAlbum));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteAlbum>()))
|
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteAlbum>(), null))
|
||||||
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -260,12 +260,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum));
|
decisions.Add(new DownloadDecision(remoteAlbum));
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum2));
|
decisions.Add(new DownloadDecision(remoteAlbum2));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)))
|
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null))
|
||||||
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null), Times.Once());
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -278,7 +278,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||||
decisions.Add(new DownloadDecision(remoteAlbum));
|
decisions.Add(new DownloadDecision(remoteAlbum));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>()
|
Mocker.GetMock<IDownloadService>()
|
||||||
.Setup(s => s.DownloadReport(It.IsAny<RemoteAlbum>()))
|
.Setup(s => s.DownloadReport(It.IsAny<RemoteAlbum>(), null))
|
||||||
.Throws(new ReleaseUnavailableException(remoteAlbum.Release, "That 404 Error is not just a Quirk"));
|
.Throws(new ReleaseUnavailableException(remoteAlbum.Release, "That 404 Error is not just a Quirk"));
|
||||||
|
|
||||||
var result = await Subject.ProcessDecisions(decisions);
|
var result = await Subject.ProcessDecisions(decisions);
|
||||||
|
|
|
@ -454,6 +454,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
|
||||||
|
|
||||||
[TestCase("0")]
|
[TestCase("0")]
|
||||||
[TestCase("15d")]
|
[TestCase("15d")]
|
||||||
|
[TestCase("")]
|
||||||
|
[TestCase(null)]
|
||||||
public void should_set_history_removes_completed_downloads_false(string historyRetention)
|
public void should_set_history_removes_completed_downloads_false(string historyRetention)
|
||||||
{
|
{
|
||||||
_config.Misc.history_retention = historyRetention;
|
_config.Misc.history_retention = historyRetention;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue