mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-18 21:34:28 -04:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2faef704b4 | |||
| a566c3e21f | |||
| cc0d2a84ae | |||
| 1c3d2ce4e5 | |||
| 57f614f4cd | |||
| 9d2efe0944 | |||
| e032be48e0 | |||
| cd66de1992 | |||
| 3066dd92d7 | |||
| 467a87baec | |||
| 80fb077c94 | |||
| 07433d69ca | |||
| 3b3ebe463c | |||
| 03392ca635 | |||
| d23ce9ecc2 | |||
| e968fcaff6 | |||
| 31da559f89 | |||
| a093290792 | |||
| 9e3dfc510d | |||
| 9d27c172ac | |||
| 518dbe53eb | |||
| f9ba00c9e7 | |||
| 4aec7a0ea7 | |||
| fc4cf8e81e | |||
| 143de3b220 | |||
| e1a07d01b2 | |||
| 27e498bb14 | |||
| b9f1882a57 | |||
| 2392573c39 | |||
| 2351efd013 | |||
| 526429bde4 | |||
| abd44b59bc | |||
| 9942457ffc | |||
| 073342ef39 | |||
| b455708f2e | |||
| 622b02c478 | |||
| 8effba383d | |||
| 2749479283 | |||
| 4cbafa76d8 | |||
| 73782cc233 | |||
| de396fe9be | |||
| 71cb9e1dd7 | |||
| ee5ed57fcc | |||
| d20a049a5a | |||
| a9f77ace37 | |||
| 0341a2ec26 | |||
| d6796bbe1a | |||
| 9066f8558c | |||
| c4e37528ee | |||
| 5937c952af | |||
| 0f4bd3c472 | |||
| cf415e61de | |||
| 9865e92cea | |||
| 1cf956a9d9 | |||
| 8989c55c8c | |||
| dc83e0127e | |||
| 34eb312426 |
+1
-1
@@ -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: '0.3.13'
|
majorVersion: '0.3.16'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
|
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,6 +44,10 @@
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filterIcon {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
.authorNavigationButtons {
|
.authorNavigationButtons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface CssExports {
|
|||||||
'authorUpButton': string;
|
'authorUpButton': string;
|
||||||
'contentContainer': string;
|
'contentContainer': string;
|
||||||
'errorMessage': string;
|
'errorMessage': string;
|
||||||
|
'filterIcon': string;
|
||||||
'innerContentBody': string;
|
'innerContentBody': string;
|
||||||
'metadataMessage': string;
|
'metadataMessage': string;
|
||||||
'selectedTab': string;
|
'selectedTab': string;
|
||||||
|
|||||||
@@ -239,9 +239,14 @@ class AuthorDetails extends Component {
|
|||||||
saveError,
|
saveError,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
deleteError,
|
deleteError,
|
||||||
statistics
|
statistics = {}
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
bookFileCount = 0,
|
||||||
|
totalBookCount = 0
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOrganizeModalOpen,
|
isOrganizeModalOpen,
|
||||||
isRetagModalOpen,
|
isRetagModalOpen,
|
||||||
@@ -435,7 +440,7 @@ class AuthorDetails extends Component {
|
|||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
{translate('BooksTotal', [statistics.totalBookCount])}
|
{translate('BooksTotal', [totalBookCount])}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
@@ -463,7 +468,7 @@ class AuthorDetails extends Component {
|
|||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
{translate('FilesTotal', [statistics.bookFileCount])}
|
{translate('FilesTotal', [bookFileCount])}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ function createMapStateToProps() {
|
|||||||
const isRefreshing = isAuthorRefreshing || allAuthorRefreshing;
|
const isRefreshing = isAuthorRefreshing || allAuthorRefreshing;
|
||||||
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.AUTHOR_SEARCH, authorId: author.id }));
|
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.AUTHOR_SEARCH, authorId: author.id }));
|
||||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
||||||
|
|
||||||
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||||
const isRenamingAuthor = (
|
const isRenamingAuthor = (
|
||||||
isCommandExecuting(isRenamingAuthorCommand) &&
|
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||||
|
|||||||
@@ -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 AuthorHistoryContentConnector from './AuthorHistoryContentConnector';
|
import AuthorHistoryContentConnector from './AuthorHistoryContentConnector';
|
||||||
import AuthorHistoryModalContent from './AuthorHistoryModalContent';
|
import AuthorHistoryModalContent from './AuthorHistoryModalContent';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ function AuthorHistoryModal(props) {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
<AuthorHistoryContentConnector
|
<AuthorHistoryContentConnector
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import AuthorHistoryTableContent from './AuthorHistoryTableContent';
|
import AuthorHistoryTableContent from './AuthorHistoryTableContent';
|
||||||
|
|
||||||
class AuthorHistoryModalContent extends Component {
|
class AuthorHistoryModalContent extends Component {
|
||||||
@@ -20,7 +21,7 @@ class AuthorHistoryModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
History
|
{translate('History')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -31,7 +32,7 @@ class AuthorHistoryModalContent extends Component {
|
|||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>
|
||||||
Close
|
{translate('Close')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
|
import BookFormats from 'Book/BookFormats';
|
||||||
import BookQuality from 'Book/BookQuality';
|
import BookQuality from 'Book/BookQuality';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
@@ -11,6 +12,7 @@ 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 { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AuthorHistoryRow.css';
|
import styles from './AuthorHistoryRow.css';
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ class AuthorHistoryRow extends Component {
|
|||||||
sourceTitle,
|
sourceTitle,
|
||||||
quality,
|
quality,
|
||||||
qualityCutoffNotMet,
|
qualityCutoffNotMet,
|
||||||
|
customFormats,
|
||||||
|
customFormatScore,
|
||||||
date,
|
date,
|
||||||
data,
|
data,
|
||||||
book
|
book
|
||||||
@@ -106,11 +110,19 @@ class AuthorHistoryRow extends Component {
|
|||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
<BookFormats 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
|
||||||
@@ -127,14 +139,13 @@ class AuthorHistoryRow extends Component {
|
|||||||
}
|
}
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
</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}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -160,6 +171,8 @@ AuthorHistoryRow.propTypes = {
|
|||||||
sourceTitle: PropTypes.string.isRequired,
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||||
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
date: PropTypes.string.isRequired,
|
date: PropTypes.string.isRequired,
|
||||||
data: PropTypes.object.isRequired,
|
data: PropTypes.object.isRequired,
|
||||||
fullAuthor: PropTypes.bool.isRequired,
|
fullAuthor: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'container': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector';
|
import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector';
|
||||||
import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent';
|
import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent';
|
||||||
|
import styles from './AuthorHistoryTable.css';
|
||||||
|
|
||||||
function AuthorHistoryTable(props) {
|
function AuthorHistoryTable(props) {
|
||||||
const {
|
const {
|
||||||
@@ -8,10 +9,12 @@ function AuthorHistoryTable(props) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthorHistoryContentConnector
|
<div className={styles.container}>
|
||||||
component={AuthorHistoryTableContent}
|
<AuthorHistoryContentConnector
|
||||||
{...otherProps}
|
component={AuthorHistoryTableContent}
|
||||||
/>
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.blankpad {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'blankpad': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
|
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
|
||||||
|
import styles from './AuthorHistoryTableContent.css';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -15,32 +17,41 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'book',
|
name: 'book',
|
||||||
label: 'Book',
|
label: () => translate('Book'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'sourceTitle',
|
name: 'sourceTitle',
|
||||||
label: 'Source Title',
|
label: () => translate( 'SourceTitle'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'quality',
|
name: 'quality',
|
||||||
label: 'Quality',
|
label: () => translate('Quality'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormats',
|
||||||
|
label: () => translate('CustomFormats'),
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormatScore',
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.SCORE,
|
||||||
|
title: () => translate('CustomFormatScore')
|
||||||
|
}),
|
||||||
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'date',
|
||||||
label: 'Date',
|
label: () => translate('Date'),
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'details',
|
|
||||||
label: 'Details',
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
label: 'Actions',
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -64,7 +75,7 @@ class AuthorHistoryTableContent extends Component {
|
|||||||
const hasItems = !!items.length;
|
const hasItems = !!items.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
{
|
{
|
||||||
isFetching &&
|
isFetching &&
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
@@ -79,7 +90,7 @@ class AuthorHistoryTableContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !hasItems && !error &&
|
isPopulated && !hasItems && !error &&
|
||||||
<div>
|
<div className={styles.blankpad}>
|
||||||
{translate('NoHistory')}
|
{translate('NoHistory')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -103,7 +114,7 @@ class AuthorHistoryTableContent extends Component {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
}
|
}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, isMonitored, isCutoffNotMet) {
|
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
||||||
const revision = quality.revision;
|
const revision = quality.revision;
|
||||||
@@ -28,6 +29,36 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
|||||||
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 BookQuality(props) {
|
function BookQuality(props) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
@@ -35,7 +66,8 @@ function BookQuality(props) {
|
|||||||
quality,
|
quality,
|
||||||
size,
|
size,
|
||||||
isMonitored,
|
isMonitored,
|
||||||
isCutoffNotMet
|
isCutoffNotMet,
|
||||||
|
showRevision
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
let kind = kinds.DEFAULT;
|
let kind = kinds.DEFAULT;
|
||||||
@@ -50,13 +82,15 @@ function BookQuality(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<span>
|
||||||
className={className}
|
<Label
|
||||||
kind={kind}
|
className={className}
|
||||||
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
kind={kind}
|
||||||
>
|
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
||||||
{quality.quality.name}
|
>
|
||||||
</Label>
|
{quality.quality.name}
|
||||||
|
</Label>{revisionLabel(className, quality, showRevision)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,12 +100,14 @@ BookQuality.propTypes = {
|
|||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
isMonitored: PropTypes.bool,
|
isMonitored: PropTypes.bool,
|
||||||
isCutoffNotMet: PropTypes.bool
|
isCutoffNotMet: PropTypes.bool,
|
||||||
|
showRevision: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
BookQuality.defaultProps = {
|
BookQuality.defaultProps = {
|
||||||
title: '',
|
title: '',
|
||||||
isMonitored: true
|
isMonitored: true,
|
||||||
|
showRevision: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BookQuality;
|
export default BookQuality;
|
||||||
|
|||||||
@@ -99,9 +99,14 @@ class BookDetails extends Component {
|
|||||||
nextBook,
|
nextBook,
|
||||||
isSearching,
|
isSearching,
|
||||||
onRefreshPress,
|
onRefreshPress,
|
||||||
onSearchPress
|
onSearchPress,
|
||||||
|
statistics = {}
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
bookFileCount = 0
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOrganizeModalOpen,
|
isOrganizeModalOpen,
|
||||||
isRetagModalOpen,
|
isRetagModalOpen,
|
||||||
@@ -238,21 +243,21 @@ class BookDetails extends Component {
|
|||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
History
|
{translate('History')}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
Search
|
{translate('Search')}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
className={styles.tab}
|
className={styles.tab}
|
||||||
selectedClassName={styles.selectedTab}
|
selectedClassName={styles.selectedTab}
|
||||||
>
|
>
|
||||||
Files
|
{translate('FilesTotal', [bookFileCount])}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -335,6 +340,7 @@ BookDetails.propTypes = {
|
|||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
statistics: PropTypes.object.isRequired,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -69,16 +69,21 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks);
|
const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks);
|
||||||
const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks);
|
const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks);
|
||||||
|
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
||||||
|
const isRefreshing = (
|
||||||
|
isCommandExecuting(isRefreshingCommand) &&
|
||||||
|
isRefreshingCommand.body.bookId === book.id
|
||||||
|
);
|
||||||
const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH });
|
const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH });
|
||||||
const isSearching = (
|
const isSearching = (
|
||||||
isCommandExecuting(isSearchingCommand) &&
|
isCommandExecuting(isSearchingCommand) &&
|
||||||
isSearchingCommand.body.bookIds.indexOf(book.id) > -1
|
isSearchingCommand.body.bookIds.indexOf(book.id) > -1
|
||||||
);
|
);
|
||||||
|
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
||||||
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||||
const isRefreshing = (
|
const isRenamingAuthor = (
|
||||||
isCommandExecuting(isRefreshingCommand) &&
|
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||||
isRefreshingCommand.body.bookId === book.id
|
isRenamingAuthorCommand.body.authorIds.indexOf(author.id) > -1
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFetching = isBookFilesFetching || editions.isFetching;
|
const isFetching = isBookFilesFetching || editions.isFetching;
|
||||||
@@ -90,6 +95,8 @@ function createMapStateToProps() {
|
|||||||
author,
|
author,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
isSearching,
|
isSearching,
|
||||||
|
isRenamingFiles,
|
||||||
|
isRenamingAuthor,
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
bookFilesError,
|
bookFilesError,
|
||||||
@@ -125,9 +132,27 @@ class BookDetailsConnector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.id !== this.props.id ||
|
const {
|
||||||
|
id,
|
||||||
|
anyReleaseOk,
|
||||||
|
isRenamingFiles,
|
||||||
|
isRenamingAuthor
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(prevProps.isRenamingFiles && !isRenamingFiles) ||
|
||||||
|
(prevProps.isRenamingAuthor && !isRenamingAuthor) ||
|
||||||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
||||||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
|
(prevProps.anyReleaseOk === false && anyReleaseOk === true)
|
||||||
|
) {
|
||||||
|
this.unpopulate();
|
||||||
|
this.populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the id has changed we need to clear the book
|
||||||
|
// files and fetch from the server.
|
||||||
|
|
||||||
|
if (prevProps.id !== id) {
|
||||||
this.unpopulate();
|
this.unpopulate();
|
||||||
this.populate();
|
this.populate();
|
||||||
}
|
}
|
||||||
@@ -197,6 +222,8 @@ class BookDetailsConnector extends Component {
|
|||||||
BookDetailsConnector.propTypes = {
|
BookDetailsConnector.propTypes = {
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
anyReleaseOk: PropTypes.bool,
|
anyReleaseOk: PropTypes.bool,
|
||||||
|
isRenamingFiles: PropTypes.bool.isRequired,
|
||||||
|
isRenamingAuthor: PropTypes.bool.isRequired,
|
||||||
isBookFetching: PropTypes.bool,
|
isBookFetching: PropTypes.bool,
|
||||||
isBookPopulated: PropTypes.bool,
|
isBookPopulated: PropTypes.bool,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ class BookIndexRow extends Component {
|
|||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{bookFileCount}
|
{bookFileCount}
|
||||||
|
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'container': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
|
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
|
||||||
|
import styles from './BookFileEditorTable.css';
|
||||||
|
|
||||||
function BookFileEditorTable(props) {
|
function BookFileEditorTable(props) {
|
||||||
const {
|
const {
|
||||||
@@ -7,9 +8,11 @@ function BookFileEditorTable(props) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookFileEditorTableContentConnector
|
<div className={styles.container}>
|
||||||
{...otherProps}
|
<BookFileEditorTableContentConnector
|
||||||
/>
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.filesTable {
|
.filesTable {
|
||||||
margin-bottom: 20px;
|
margin: 10px;
|
||||||
padding-top: 15px;
|
padding-top: 5px;
|
||||||
border: 1px solid var(--borderColor);
|
border: 1px solid var(--borderColor);
|
||||||
border-top: 1px solid var(--borderColor);
|
border-top: 1px solid var(--borderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -13,9 +13,15 @@
|
|||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: auto;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectInput {
|
.selectInput {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blankpad {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
|
'blankpad': string;
|
||||||
'filesTable': string;
|
'filesTable': string;
|
||||||
'selectInput': string;
|
'selectInput': string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import SelectInput from 'Components/Form/SelectInput';
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -120,7 +121,7 @@ class BookFileEditorTableContent extends Component {
|
|||||||
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
{
|
{
|
||||||
isFetching && !isPopulated ?
|
isFetching && !isPopulated ?
|
||||||
<LoadingIndicator /> :
|
<LoadingIndicator /> :
|
||||||
@@ -129,13 +130,13 @@ class BookFileEditorTableContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && error ?
|
!isFetching && error ?
|
||||||
<div>{error}</div> :
|
<Alert kind={kinds.DANGER}>{error}</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !items.length ?
|
isPopulated && !items.length ?
|
||||||
<div>
|
<div className={styles.blankpad}>
|
||||||
No book files to manage.
|
No book files to manage.
|
||||||
</div> :
|
</div> :
|
||||||
null
|
null
|
||||||
@@ -173,26 +174,30 @@ class BookFileEditorTableContent extends Component {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.actions}>
|
{
|
||||||
<SpinnerButton
|
isPopulated && items.length ? (
|
||||||
kind={kinds.DANGER}
|
<div className={styles.actions}>
|
||||||
isSpinning={isDeleting}
|
<SpinnerButton
|
||||||
isDisabled={!hasSelectedFiles}
|
kind={kinds.DANGER}
|
||||||
onPress={this.onDeletePress}
|
isSpinning={isDeleting}
|
||||||
>
|
isDisabled={!hasSelectedFiles}
|
||||||
Delete
|
onPress={this.onDeletePress}
|
||||||
</SpinnerButton>
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
<div className={styles.selectInput}>
|
<div className={styles.selectInput}>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
name="quality"
|
name="quality"
|
||||||
value="selectQuality"
|
value="selectQuality"
|
||||||
values={qualityOptions}
|
values={qualityOptions}
|
||||||
isDisabled={!hasSelectedFiles}
|
isDisabled={!hasSelectedFiles}
|
||||||
onChange={this.onQualityChange}
|
onChange={this.onQualityChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isConfirmDeleteModalOpen}
|
isOpen={isConfirmDeleteModalOpen}
|
||||||
@@ -203,7 +208,7 @@ class BookFileEditorTableContent extends Component {
|
|||||||
onConfirm={this.onConfirmDelete}
|
onConfirm={this.onConfirmDelete}
|
||||||
onCancel={this.onConfirmDeleteModalClose}
|
onCancel={this.onConfirmDeleteModalClose}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,22 +29,24 @@ function CustomFiltersModalContent(props) {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters.map((customFilter) => {
|
customFilters
|
||||||
return (
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
<CustomFilter
|
.map((customFilter) => {
|
||||||
key={customFilter.id}
|
return (
|
||||||
id={customFilter.id}
|
<CustomFilter
|
||||||
label={customFilter.label}
|
key={customFilter.id}
|
||||||
filters={customFilter.filters}
|
id={customFilter.id}
|
||||||
selectedFilterKey={selectedFilterKey}
|
label={customFilter.label}
|
||||||
isDeleting={isDeleting}
|
filters={customFilter.filters}
|
||||||
deleteError={deleteError}
|
selectedFilterKey={selectedFilterKey}
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
isDeleting={isDeleting}
|
||||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
deleteError={deleteError}
|
||||||
onEditPress={onEditCustomFilter}
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
/>
|
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||||
);
|
onEditPress={onEditCustomFilter}
|
||||||
})
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.addButtonContainer}>
|
<div className={styles.addButtonContainer}>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -39,18 +39,26 @@ class FilterMenuContent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
customFilters.map((filter) => {
|
customFilters.length > 0 ?
|
||||||
return (
|
<MenuItemSeparator /> :
|
||||||
<FilterMenuItem
|
null
|
||||||
key={filter.id}
|
}
|
||||||
filterKey={filter.id}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
{
|
||||||
onPress={onFilterSelect}
|
customFilters
|
||||||
>
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
{filter.label}
|
.map((filter) => {
|
||||||
</FilterMenuItem>
|
return (
|
||||||
);
|
<FilterMenuItem
|
||||||
})
|
key={filter.id}
|
||||||
|
filterKey={filter.id}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
onPress={onFilterSelect}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</FilterMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,6 +28,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}>
|
||||||
<BookQuality quality={quality} />
|
<BookQuality quality={quality} showRevision={true} />
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
|
|||||||
+16
@@ -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,8 +61,12 @@ function DownloadClientOptions(props) {
|
|||||||
legend={translate('FailedDownloadHandling')}
|
legend={translate('FailedDownloadHandling')}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup
|
||||||
<FormLabel>{translate('RedownloadFailed')}</FormLabel>
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
@@ -72,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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -223,6 +223,13 @@ class UISettings extends Component {
|
|||||||
helpTextWarning={translate('UILanguageHelpTextWarning')}
|
helpTextWarning={translate('UILanguageHelpTextWarning')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.uiLanguage}
|
{...settings.uiLanguage}
|
||||||
|
errors={
|
||||||
|
languages.some((language) => language.key === settings.uiLanguage.value) ?
|
||||||
|
settings.uiLanguage.errors :
|
||||||
|
[
|
||||||
|
...settings.uiLanguage.errors,
|
||||||
|
{ message: translate('InvalidUILanguage') }
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|||||||
@@ -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,9 @@ export default {
|
|||||||
isTesting: false,
|
isTesting: false,
|
||||||
isTestingAll: false,
|
isTestingAll: false,
|
||||||
items: [],
|
items: [],
|
||||||
pendingChanges: {}
|
pendingChanges: {},
|
||||||
|
sortKey: 'name',
|
||||||
|
sortDirection: sortDirections.DESCENDING
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -121,7 +127,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,9 @@ export default {
|
|||||||
isTesting: false,
|
isTesting: false,
|
||||||
isTestingAll: false,
|
isTestingAll: false,
|
||||||
items: [],
|
items: [],
|
||||||
pendingChanges: {}
|
pendingChanges: {},
|
||||||
|
sortKey: 'name',
|
||||||
|
sortDirection: sortDirections.DESCENDING
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -151,7 +157,10 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
return updateSectionState(state, section, newState);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
columns: [
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'select',
|
||||||
|
columnLabel: 'Select',
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: true,
|
||||||
|
isModifiable: false,
|
||||||
|
isHidden: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'path',
|
name: 'path',
|
||||||
label: 'Path',
|
label: 'Path',
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const defaultState = {
|
|||||||
bookFileCount: function(item) {
|
bookFileCount: function(item) {
|
||||||
const { statistics = {} } = item;
|
const { statistics = {} } = item;
|
||||||
|
|
||||||
return statistics.bookCount || 0;
|
return statistics.bookFileCount || 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
ratings: function(item) {
|
ratings: function(item) {
|
||||||
|
|||||||
@@ -84,11 +84,6 @@ export const defaultState = {
|
|||||||
label: 'Source Title',
|
label: 'Source Title',
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'sourceTitle',
|
|
||||||
label: 'Source Title',
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'customFormatScore',
|
name: 'customFormatScore',
|
||||||
columnLabel: 'Custom Format Score',
|
columnLabel: 'Custom Format Score',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
@@ -9,8 +10,12 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import VirtualTable from 'Components/Table/VirtualTable';
|
import VirtualTable from 'Components/Table/VirtualTable';
|
||||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import UnmappedFilesTableHeader from './UnmappedFilesTableHeader';
|
import UnmappedFilesTableHeader from './UnmappedFilesTableHeader';
|
||||||
import UnmappedFilesTableRow from './UnmappedFilesTableRow';
|
import UnmappedFilesTableRow from './UnmappedFilesTableRow';
|
||||||
|
|
||||||
@@ -23,10 +28,43 @@ class UnmappedFilesTable extends Component {
|
|||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
scroller: null
|
scroller: null,
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setSelectedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
isDeleting,
|
||||||
|
deleteError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (sortKey !== prevProps.sortKey ||
|
||||||
|
sortDirection !== prevProps.sortDirection ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||||
|
) {
|
||||||
|
this.setSelectedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFinishedDeleting = prevProps.isDeleting &&
|
||||||
|
!isDeleting &&
|
||||||
|
!deleteError;
|
||||||
|
|
||||||
|
if (hasFinishedDeleting) {
|
||||||
|
this.onSelectAllChange({ value: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
@@ -34,6 +72,68 @@ class UnmappedFilesTable extends Component {
|
|||||||
this.setState({ scroller: ref });
|
this.setState({ scroller: ref });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
if (this.state.allUnselected) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedState() {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedState
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const newSelectedState = {};
|
||||||
|
|
||||||
|
items.forEach((file) => {
|
||||||
|
const isItemSelected = selectedState[file.id];
|
||||||
|
|
||||||
|
if (isItemSelected) {
|
||||||
|
newSelectedState[file.id] = isItemSelected;
|
||||||
|
} else {
|
||||||
|
newSelectedState[file.id] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||||
|
const newStateCount = Object.keys(newSelectedState).length;
|
||||||
|
let isAllSelected = false;
|
||||||
|
let isAllUnselected = false;
|
||||||
|
|
||||||
|
if (selectedCount === 0) {
|
||||||
|
isAllUnselected = true;
|
||||||
|
} else if (selectedCount === newStateCount) {
|
||||||
|
isAllSelected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAllPress = () => {
|
||||||
|
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteUnmappedFilesPress = () => {
|
||||||
|
const selectedIds = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.deleteUnmappedFiles(selectedIds);
|
||||||
|
};
|
||||||
|
|
||||||
rowRenderer = ({ key, rowIndex, style }) => {
|
rowRenderer = ({ key, rowIndex, style }) => {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
@@ -41,6 +141,10 @@ class UnmappedFilesTable extends Component {
|
|||||||
deleteUnmappedFile
|
deleteUnmappedFile
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedState
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
const item = items[rowIndex];
|
const item = items[rowIndex];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,6 +155,8 @@ class UnmappedFilesTable extends Component {
|
|||||||
<UnmappedFilesTableRow
|
<UnmappedFilesTableRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
deleteUnmappedFile={deleteUnmappedFile}
|
deleteUnmappedFile={deleteUnmappedFile}
|
||||||
{...item}
|
{...item}
|
||||||
/>
|
/>
|
||||||
@@ -63,6 +169,7 @@ class UnmappedFilesTable extends Component {
|
|||||||
const {
|
const {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
|
isDeleting,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
@@ -72,13 +179,19 @@ class UnmappedFilesTable extends Component {
|
|||||||
onSortPress,
|
onSortPress,
|
||||||
isScanningFolders,
|
isScanningFolders,
|
||||||
onAddMissingAuthorsPress,
|
onAddMissingAuthorsPress,
|
||||||
|
deleteUnmappedFiles,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
scroller
|
scroller,
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const selectedTrackFileIds = this.getSelectedIds();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title={translate('UnmappedFiles')}>
|
<PageContent title={translate('UnmappedFiles')}>
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
@@ -90,6 +203,13 @@ class UnmappedFilesTable extends Component {
|
|||||||
isSpinning={isScanningFolders}
|
isSpinning={isScanningFolders}
|
||||||
onPress={onAddMissingAuthorsPress}
|
onPress={onAddMissingAuthorsPress}
|
||||||
/>
|
/>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('DeleteSelected')}
|
||||||
|
iconName={icons.DELETE}
|
||||||
|
isDisabled={selectedTrackFileIds.length === 0}
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
onPress={this.onDeleteUnmappedFilesPress}
|
||||||
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
@@ -117,9 +237,9 @@ class UnmappedFilesTable extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !error && !items.length &&
|
isPopulated && !error && !items.length &&
|
||||||
<div>
|
<Alert kind={kinds.INFO}>
|
||||||
Success! My work is done, all files on disk are matched to known books.
|
Success! My work is done, all files on disk are matched to known books.
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -138,8 +258,12 @@ class UnmappedFilesTable extends Component {
|
|||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onTableOptionChange={onTableOptionChange}
|
onTableOptionChange={onTableOptionChange}
|
||||||
onSortPress={onSortPress}
|
onSortPress={onSortPress}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
selectedState={selectedState}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
/>
|
/>
|
||||||
@@ -153,6 +277,8 @@ class UnmappedFilesTable extends Component {
|
|||||||
UnmappedFilesTable.propTypes = {
|
UnmappedFilesTable.propTypes = {
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
|
deleteError: PropTypes.object,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
@@ -161,6 +287,7 @@ UnmappedFilesTable.propTypes = {
|
|||||||
onTableOptionChange: PropTypes.func.isRequired,
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
deleteUnmappedFile: PropTypes.func.isRequired,
|
deleteUnmappedFile: PropTypes.func.isRequired,
|
||||||
|
deleteUnmappedFiles: PropTypes.func.isRequired,
|
||||||
isScanningFolders: PropTypes.bool.isRequired,
|
isScanningFolders: PropTypes.bool.isRequired,
|
||||||
onAddMissingAuthorsPress: PropTypes.func.isRequired
|
onAddMissingAuthorsPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { deleteBookFile, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions';
|
import { deleteBookFile, deleteBookFiles, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
@@ -28,7 +28,9 @@ function createMapStateToProps() {
|
|||||||
items,
|
items,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = bookFiles;
|
} = bookFiles;
|
||||||
|
|
||||||
const unmappedFiles = _.filter(items, { bookId: 0 });
|
const unmappedFiles = _.filter(items, { bookId: 0 });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: unmappedFiles,
|
items: unmappedFiles,
|
||||||
...otherProps,
|
...otherProps,
|
||||||
@@ -57,6 +59,10 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatch(deleteBookFile({ id }));
|
dispatch(deleteBookFile({ id }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteUnmappedFiles(bookFileIds) {
|
||||||
|
dispatch(deleteBookFiles({ bookFileIds }));
|
||||||
|
},
|
||||||
|
|
||||||
onAddMissingAuthorsPress() {
|
onAddMissingAuthorsPress() {
|
||||||
dispatch(executeCommand({
|
dispatch(executeCommand({
|
||||||
name: commandNames.RESCAN_FOLDERS,
|
name: commandNames.RESCAN_FOLDERS,
|
||||||
@@ -106,7 +112,8 @@ UnmappedFilesTableConnector.propTypes = {
|
|||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
onTableOptionChange: PropTypes.func.isRequired,
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
fetchUnmappedFiles: PropTypes.func.isRequired,
|
fetchUnmappedFiles: PropTypes.func.isRequired,
|
||||||
deleteUnmappedFile: PropTypes.func.isRequired
|
deleteUnmappedFile: PropTypes.func.isRequired,
|
||||||
|
deleteUnmappedFiles: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withCurrentPage(
|
export default withCurrentPage(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
|
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
// import hasGrowableColumns from './hasGrowableColumns';
|
// import hasGrowableColumns from './hasGrowableColumns';
|
||||||
import styles from './UnmappedFilesTableHeader.css';
|
import styles from './UnmappedFilesTableHeader.css';
|
||||||
@@ -12,6 +13,9 @@ function UnmappedFilesTableHeader(props) {
|
|||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
onTableOptionChange,
|
onTableOptionChange,
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
onSelectAllChange,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -30,6 +34,17 @@ function UnmappedFilesTableHeader(props) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'select') {
|
||||||
|
return (
|
||||||
|
<VirtualTableSelectAllHeaderCell
|
||||||
|
key={name}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableHeaderCell
|
<VirtualTableHeaderCell
|
||||||
@@ -71,6 +86,9 @@ function UnmappedFilesTableHeader(props) {
|
|||||||
|
|
||||||
UnmappedFilesTableHeader.propTypes = {
|
UnmappedFilesTableHeader.propTypes = {
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
allSelected: PropTypes.bool.isRequired,
|
||||||
|
allUnselected: PropTypes.bool.isRequired,
|
||||||
|
onSelectAllChange: PropTypes.func.isRequired,
|
||||||
onTableOptionChange: PropTypes.func.isRequired
|
onTableOptionChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,9 @@
|
|||||||
|
|
||||||
flex: 0 0 100px;
|
flex: 0 0 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkInput {
|
||||||
|
composes: input from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
|
'checkInput': string;
|
||||||
'dateAdded': string;
|
'dateAdded': string;
|
||||||
'path': string;
|
'path': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
@@ -69,7 +70,9 @@ class UnmappedFilesTableRow extends Component {
|
|||||||
size,
|
size,
|
||||||
dateAdded,
|
dateAdded,
|
||||||
quality,
|
quality,
|
||||||
columns
|
columns,
|
||||||
|
isSelected,
|
||||||
|
onSelectedChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
|
const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
|
||||||
@@ -93,6 +96,19 @@ class UnmappedFilesTableRow extends Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'select') {
|
||||||
|
return (
|
||||||
|
<VirtualTableSelectCell
|
||||||
|
inputClassName={styles.checkInput}
|
||||||
|
id={id}
|
||||||
|
key={name}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isDisabled={false}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'path') {
|
if (name === 'path') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell
|
<VirtualTableRowCell
|
||||||
@@ -208,6 +224,8 @@ UnmappedFilesTableRow.propTypes = {
|
|||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
dateAdded: PropTypes.string.isRequired,
|
dateAdded: PropTypes.string.isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
deleteUnmappedFile: PropTypes.func.isRequired
|
deleteUnmappedFile: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,20 +25,18 @@ export async function fetchTranslations(): Promise<boolean> {
|
|||||||
|
|
||||||
export default function translate(
|
export default function translate(
|
||||||
key: string,
|
key: string,
|
||||||
tokens: Record<string, string | number | boolean> = { appName: 'Readarr' }
|
tokens: Record<string, string | number | boolean> = {}
|
||||||
) {
|
) {
|
||||||
const translation = translations[key] || key;
|
const translation = translations[key] || key;
|
||||||
|
|
||||||
if (tokens) {
|
tokens.appName = 'Readarr';
|
||||||
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
|
||||||
Object.values(tokens).forEach((value, index) => {
|
|
||||||
tokens[index] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
||||||
String(tokens[tokenMatch] ?? match)
|
Object.values(tokens).forEach((value, index) => {
|
||||||
);
|
tokens[index] = value;
|
||||||
}
|
});
|
||||||
|
|
||||||
return translation;
|
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||||
|
String(tokens[tokenMatch] ?? match)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||||
<PackageVersion Include="Equ" Version="2.3.0" />
|
<PackageVersion Include="Equ" Version="2.3.0" />
|
||||||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||||
|
<PackageVersion Include="Polly" Version="8.2.0" />
|
||||||
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
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;
|
||||||
using Readarr.Http.ClientSchema;
|
using Readarr.Http.ClientSchema;
|
||||||
|
|
||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -124,6 +124,16 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
response.Content.Should().NotBeNullOrWhiteSpace();
|
response.Content.Should().NotBeNullOrWhiteSpace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_timeout_request()
|
||||||
|
{
|
||||||
|
var request = new HttpRequest($"https://{_httpBinHost}/delay/10");
|
||||||
|
|
||||||
|
request.RequestTimeout = new TimeSpan(0, 0, 5);
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<WebException>(async () => await Subject.ExecuteAsync(request));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task should_execute_https_get()
|
public async Task should_execute_https_get()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -103,31 +103,38 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
|
|
||||||
var httpClient = GetClient(request.Url);
|
var httpClient = GetClient(request.Url);
|
||||||
|
|
||||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
try
|
||||||
{
|
{
|
||||||
byte[] data = null;
|
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
byte[] data = null;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var headers = responseMessage.Headers.ToNameValueCollection();
|
||||||
|
|
||||||
|
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
||||||
|
|
||||||
|
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
|
||||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
{
|
||||||
}
|
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
|
||||||
|
|
||||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
|
||||||
|
|
||||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
|
||||||
|
|
||||||
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -101,7 +101,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook2));
|
decisions.Add(new DownloadDecision(remoteBook2));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), 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(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>())).Throws(new Exception());
|
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>(), 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(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
|
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Never());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Never());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -242,11 +242,11 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
|
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>(), 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<RemoteBook>()), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -260,12 +260,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
decisions.Add(new DownloadDecision(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
decisions.Add(new DownloadDecision(remoteBook2));
|
decisions.Add(new DownloadDecision(remoteBook2));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)))
|
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteBook>(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<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null), Times.Once());
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(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(remoteBook));
|
decisions.Add(new DownloadDecision(remoteBook));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadService>()
|
Mocker.GetMock<IDownloadService>()
|
||||||
.Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
|
.Setup(s => s.DownloadReport(It.IsAny<RemoteBook>(), null))
|
||||||
.Throws(new ReleaseUnavailableException(remoteBook.Release, "That 404 Error is not just a Quirk"));
|
.Throws(new ReleaseUnavailableException(remoteBook.Release, "That 404 Error is not just a Quirk"));
|
||||||
|
|
||||||
var result = await Subject.ProcessDecisions(decisions);
|
var result = await Subject.ProcessDecisions(decisions);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
var mock = WithUsenetClient();
|
var mock = WithUsenetClient();
|
||||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
VerifyEventPublished<BookGrabbedEvent>();
|
VerifyEventPublished<BookGrabbedEvent>();
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
var mock = WithUsenetClient();
|
var mock = WithUsenetClient();
|
||||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
mock.Verify(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
mock.Verify(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
||||||
.Throws(new WebException());
|
.Throws(new WebException());
|
||||||
|
|
||||||
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
VerifyEventNotPublished<BookGrabbedEvent>();
|
VerifyEventNotPublished<BookGrabbedEvent>();
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
throw new ReleaseDownloadException(v.Release, "Error", new WebException());
|
throw new ReleaseDownloadException(v.Release, "Error", new WebException());
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Once());
|
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Once());
|
||||||
@@ -141,7 +141,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5.0)), Times.Once());
|
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5.0)), Times.Once());
|
||||||
@@ -161,7 +161,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(),
|
.Verify(v => v.RecordFailure(It.IsAny<int>(),
|
||||||
@@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
||||||
.Throws(new DownloadClientException("Some Error"));
|
.Throws(new DownloadClientException("Some Error"));
|
||||||
|
|
||||||
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
||||||
@@ -191,7 +191,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
throw new ReleaseUnavailableException(v.Release, "Error", new WebException());
|
throw new ReleaseUnavailableException(v.Release, "Error", new WebException());
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerStatusService>()
|
Mocker.GetMock<IIndexerStatusService>()
|
||||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
||||||
@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_not_attempt_download_if_client_isnt_configured()
|
public void should_not_attempt_download_if_client_isnt_configured()
|
||||||
{
|
{
|
||||||
Assert.ThrowsAsync<DownloadClientUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
|
Assert.ThrowsAsync<DownloadClientUnavailableException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadClient>().Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
Mocker.GetMock<IDownloadClient>().Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||||
VerifyEventNotPublished<BookGrabbedEvent>();
|
VerifyEventNotPublished<BookGrabbedEvent>();
|
||||||
@@ -222,7 +222,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
Mocker.GetMock<IDownloadClientStatusService>().Verify(c => c.GetBlockedProviders(), Times.Never());
|
Mocker.GetMock<IDownloadClientStatusService>().Verify(c => c.GetBlockedProviders(), Times.Never());
|
||||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||||
@@ -235,7 +235,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
var mockTorrent = WithTorrentClient();
|
var mockTorrent = WithTorrentClient();
|
||||||
var mockUsenet = WithUsenetClient();
|
var mockUsenet = WithUsenetClient();
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||||
@@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
|
|
||||||
_parseResult.Release.DownloadProtocol = DownloadProtocol.Torrent;
|
_parseResult.Release.DownloadProtocol = DownloadProtocol.Torrent;
|
||||||
|
|
||||||
await Subject.DownloadReport(_parseResult);
|
await Subject.DownloadReport(_parseResult, null);
|
||||||
|
|
||||||
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
|||||||
|
|
||||||
results.Should().NotBeEmpty();
|
results.Should().NotBeEmpty();
|
||||||
Mocker.GetMock<IMakeDownloadDecision>()
|
Mocker.GetMock<IMakeDownloadDecision>()
|
||||||
.Verify(v => v.GetRssDecision(It.Is<List<ReleaseInfo>>(d => d.Count == 0)), Times.Never());
|
.Verify(v => v.GetRssDecision(It.Is<List<ReleaseInfo>>(d => d.Count == 0), It.IsAny<bool>()), Times.Never());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
|
|
||||||
Mocker.GetMock<IHttpClient>()
|
Mocker.GetMock<IHttpClient>()
|
||||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
|
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, recentFeed)));
|
||||||
|
|
||||||
var releases = await Subject.FetchRecent();
|
var releases = await Subject.FetchRecent();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Localization
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class LocalizationServiceFixture : CoreTest<LocalizationService>
|
||||||
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.English);
|
||||||
|
|
||||||
|
Mocker.GetMock<IAppFolderInfo>().Setup(m => m.StartUpFolder).Returns(TestContext.CurrentContext.TestDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_get_string_in_dictionary_if_lang_exists_and_string_exists()
|
||||||
|
{
|
||||||
|
var localizedString = Subject.GetLocalizedString("UiLanguage");
|
||||||
|
|
||||||
|
localizedString.Should().Be("UI Language");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_get_string_in_french()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.French);
|
||||||
|
|
||||||
|
var localizedString = Subject.GetLocalizedString("UiLanguage");
|
||||||
|
|
||||||
|
localizedString.Should().Be("Langue de l'IU");
|
||||||
|
|
||||||
|
ExceptionVerification.ExpectedErrors(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns(0);
|
||||||
|
var localizedString = Subject.GetLocalizedString("UiLanguage");
|
||||||
|
|
||||||
|
localizedString.Should().Be("UI Language");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_argument_if_string_doesnt_exists()
|
||||||
|
{
|
||||||
|
var localizedString = Subject.GetLocalizedString("badString");
|
||||||
|
|
||||||
|
localizedString.Should().Be("badString");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_argument_if_string_doesnt_exists_default_lang()
|
||||||
|
{
|
||||||
|
var localizedString = Subject.GetLocalizedString("badString");
|
||||||
|
|
||||||
|
localizedString.Should().Be("badString");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_if_empty_string_passed()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => Subject.GetLocalizedString(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_if_null_string_passed()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => Subject.GetLocalizedString(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
|
|||||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
[Ignore("Waiting for metadata to be back again", Until = "2023-12-31 00:00:00Z")]
|
[Ignore("Waiting for metadata to be back again", Until = "2024-01-31 00:00:00Z")]
|
||||||
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
||||||
{
|
{
|
||||||
private MetadataProfile _metadataProfile;
|
private MetadataProfile _metadataProfile;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
|
|||||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
[Ignore("Waiting for metadata to be back again", Until = "2023-12-31 00:00:00Z")]
|
[Ignore("Waiting for metadata to be back again", Until = "2024-01-31 00:00:00Z")]
|
||||||
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
||||||
{
|
{
|
||||||
[SetUp]
|
[SetUp]
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
|||||||
|
|
||||||
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
|
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
|
||||||
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
|
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
|
||||||
.Excluding(x => x.AuthorId);
|
.Excluding(x => x.AuthorId)
|
||||||
|
.Excluding(x => x.ForeignEditionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,23 @@ namespace NzbDrone.Core.Annotations
|
|||||||
public string Hint { get; set; }
|
public string Hint { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
||||||
|
public class FieldTokenAttribute : Attribute
|
||||||
|
{
|
||||||
|
public FieldTokenAttribute(TokenField field, string label = "", string token = "", object value = null)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
Field = field;
|
||||||
|
Token = token;
|
||||||
|
Value = value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Label { get; set; }
|
||||||
|
public TokenField Field { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class FieldSelectOption
|
public class FieldSelectOption
|
||||||
{
|
{
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
@@ -83,4 +100,11 @@ namespace NzbDrone.Core.Annotations
|
|||||||
ApiKey,
|
ApiKey,
|
||||||
UserName
|
UserName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum TokenField
|
||||||
|
{
|
||||||
|
Label,
|
||||||
|
HelpText,
|
||||||
|
HelpTextWarning
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
public interface IBlocklistService
|
public interface IBlocklistService
|
||||||
{
|
{
|
||||||
bool Blocklisted(int authorId, ReleaseInfo release);
|
bool Blocklisted(int authorId, ReleaseInfo release);
|
||||||
|
bool BlocklistedTorrentHash(int authorId, string hash);
|
||||||
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
||||||
void Block(RemoteBook remoteEpisode, string message);
|
void Block(RemoteBook remoteEpisode, string message);
|
||||||
void Delete(int id);
|
void Delete(int id);
|
||||||
@@ -36,30 +37,34 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
|
|
||||||
public bool Blocklisted(int authorId, ReleaseInfo release)
|
public bool Blocklisted(int authorId, ReleaseInfo release)
|
||||||
{
|
{
|
||||||
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(authorId, release.Title);
|
|
||||||
|
|
||||||
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
||||||
{
|
{
|
||||||
var torrentInfo = release as TorrentInfo;
|
if (release is not TorrentInfo torrentInfo)
|
||||||
|
|
||||||
if (torrentInfo == null)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torrentInfo.InfoHash.IsNullOrWhiteSpace())
|
if (torrentInfo.InfoHash.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, torrentInfo.InfoHash);
|
||||||
.Any(b => SameTorrent(b, torrentInfo));
|
|
||||||
|
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, torrentInfo.InfoHash);
|
return _blocklistRepository.BlocklistedByTitle(authorId, release.Title)
|
||||||
|
.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
||||||
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
.Any(b => SameTorrent(b, torrentInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet)
|
return _blocklistRepository.BlocklistedByTitle(authorId, release.Title)
|
||||||
.Any(b => SameNzb(b, release));
|
.Where(b => b.Protocol == DownloadProtocol.Usenet)
|
||||||
|
.Any(b => SameNzb(b, release));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BlocklistedTorrentHash(int authorId, string hash)
|
||||||
|
{
|
||||||
|
return _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, hash).Any(b =>
|
||||||
|
b.TorrentInfoHash.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)
|
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)
|
||||||
|
|||||||
@@ -219,11 +219,11 @@ namespace NzbDrone.Core.Books.Calibre
|
|||||||
double? seriesIndex = null;
|
double? seriesIndex = null;
|
||||||
if (double.TryParse(serieslink?.Position, out var index))
|
if (double.TryParse(serieslink?.Position, out var index))
|
||||||
{
|
{
|
||||||
_logger.Trace($"Parsed {serieslink?.Position} as {index}");
|
_logger.Trace("Parsed '{0}' as '{1}'", serieslink.Position, index);
|
||||||
seriesIndex = index;
|
seriesIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Trace($"Book: {book} Series: {series?.Title}, Position: {seriesIndex}");
|
_logger.Trace("Book: {0} Series: {1}, Position: {2}", book, series?.Title, seriesIndex);
|
||||||
|
|
||||||
var cover = edition.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover);
|
var cover = edition.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover);
|
||||||
string image = null;
|
string image = null;
|
||||||
@@ -276,7 +276,9 @@ namespace NzbDrone.Core.Books.Calibre
|
|||||||
|
|
||||||
var updatedPath = GetOriginalFormat(updated.Formats);
|
var updatedPath = GetOriginalFormat(updated.Formats);
|
||||||
|
|
||||||
if (updatedPath != null && updatedPath != file.Path)
|
_logger.Trace("File path from Calibre: '{0}'", updatedPath);
|
||||||
|
|
||||||
|
if (updatedPath.IsNotNullOrWhiteSpace() && updatedPath != file.Path)
|
||||||
{
|
{
|
||||||
_rootFolderWatchingService.ReportFileSystemChangeBeginning(updatedPath);
|
_rootFolderWatchingService.ReportFileSystemChangeBeginning(updatedPath);
|
||||||
file.Path = updatedPath;
|
file.Path = updatedPath;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ namespace NzbDrone.Core.Books
|
|||||||
// These are metadata entries
|
// These are metadata entries
|
||||||
public int AuthorMetadataId { get; set; }
|
public int AuthorMetadataId { get; set; }
|
||||||
public string ForeignBookId { get; set; }
|
public string ForeignBookId { get; set; }
|
||||||
|
public string ForeignEditionId { get; set; }
|
||||||
public string TitleSlug { get; set; }
|
public string TitleSlug { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public DateTime? ReleaseDate { get; set; }
|
public DateTime? ReleaseDate { get; set; }
|
||||||
@@ -71,6 +72,7 @@ namespace NzbDrone.Core.Books
|
|||||||
public override void UseMetadataFrom(Book other)
|
public override void UseMetadataFrom(Book other)
|
||||||
{
|
{
|
||||||
ForeignBookId = other.ForeignBookId;
|
ForeignBookId = other.ForeignBookId;
|
||||||
|
ForeignEditionId = other.ForeignEditionId;
|
||||||
TitleSlug = other.TitleSlug;
|
TitleSlug = other.TitleSlug;
|
||||||
Title = other.Title;
|
Title = other.Title;
|
||||||
ReleaseDate = other.ReleaseDate;
|
ReleaseDate = other.ReleaseDate;
|
||||||
@@ -95,6 +97,7 @@ namespace NzbDrone.Core.Books
|
|||||||
public override void ApplyChanges(Book other)
|
public override void ApplyChanges(Book other)
|
||||||
{
|
{
|
||||||
ForeignBookId = other.ForeignBookId;
|
ForeignBookId = other.ForeignBookId;
|
||||||
|
ForeignEditionId = other.ForeignEditionId;
|
||||||
AddOptions = other.AddOptions;
|
AddOptions = other.AddOptions;
|
||||||
Monitored = other.Monitored;
|
Monitored = other.Monitored;
|
||||||
AnyEditionOk = other.AnyEditionOk;
|
AnyEditionOk = other.AnyEditionOk;
|
||||||
|
|||||||
@@ -329,8 +329,8 @@ namespace NzbDrone.Core.Configuration
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If SSL is enabled and a cert hash is still in the config file disable SSL
|
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
|
||||||
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
|
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||||
{
|
{
|
||||||
SetValue("EnableSsl", false);
|
SetValue("EnableSsl", false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ namespace NzbDrone.Core.Configuration
|
|||||||
set { SetValue("AutoRedownloadFailed", value); }
|
set { SetValue("AutoRedownloadFailed", value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool AutoRedownloadFailedFromInteractiveSearch
|
||||||
|
{
|
||||||
|
get { return GetValueBoolean("AutoRedownloadFailedFromInteractiveSearch", true); }
|
||||||
|
|
||||||
|
set { SetValue("AutoRedownloadFailedFromInteractiveSearch", value); }
|
||||||
|
}
|
||||||
|
|
||||||
public bool CreateEmptyAuthorFolders
|
public bool CreateEmptyAuthorFolders
|
||||||
{
|
{
|
||||||
get { return GetValueBoolean("CreateEmptyAuthorFolders", false); }
|
get { return GetValueBoolean("CreateEmptyAuthorFolders", false); }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
//Completed/Failed Download Handling (Download client)
|
//Completed/Failed Download Handling (Download client)
|
||||||
bool EnableCompletedDownloadHandling { get; set; }
|
bool EnableCompletedDownloadHandling { get; set; }
|
||||||
bool AutoRedownloadFailed { get; set; }
|
bool AutoRedownloadFailed { get; set; }
|
||||||
|
bool AutoRedownloadFailedFromInteractiveSearch { get; set; }
|
||||||
|
|
||||||
//Media Management
|
//Media Management
|
||||||
bool AutoUnmonitorPreviouslyDownloadedBooks { get; set; }
|
bool AutoUnmonitorPreviouslyDownloadedBooks { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Data.SQLite;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using NLog;
|
using NLog;
|
||||||
@@ -40,14 +41,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
using (var db = _datamapperFactory())
|
using (var db = _datamapperFactory())
|
||||||
{
|
{
|
||||||
if (db.ConnectionString.Contains(".db"))
|
return db is SQLiteConnection ? DatabaseType.SQLite : DatabaseType.PostgreSQL;
|
||||||
{
|
|
||||||
return DatabaseType.SQLite;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return DatabaseType.PostgreSQL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
|
|
||||||
Mapper.Entity<Book>("Books").RegisterModel()
|
Mapper.Entity<Book>("Books").RegisterModel()
|
||||||
.Ignore(x => x.AuthorId)
|
.Ignore(x => x.AuthorId)
|
||||||
|
.Ignore(x => x.ForeignEditionId)
|
||||||
.HasOne(r => r.AuthorMetadata, r => r.AuthorMetadataId)
|
.HasOne(r => r.AuthorMetadata, r => r.AuthorMetadataId)
|
||||||
.LazyLoad(x => x.BookFiles,
|
.LazyLoad(x => x.BookFiles,
|
||||||
(db, book) => db.Query<BookFile>(new SqlBuilder(db.DatabaseType)
|
(db, book) => db.Query<BookFile>(new SqlBuilder(db.DatabaseType)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
{
|
{
|
||||||
public interface IMakeDownloadDecision
|
public interface IMakeDownloadDecision
|
||||||
{
|
{
|
||||||
List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports);
|
List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports, bool pushedRelease = false);
|
||||||
List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase);
|
List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,17 +42,17 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports)
|
public List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports, bool pushedRelease = false)
|
||||||
{
|
{
|
||||||
return GetBookDecisions(reports).ToList();
|
return GetBookDecisions(reports).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase)
|
public List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase)
|
||||||
{
|
{
|
||||||
return GetBookDecisions(reports, searchCriteriaBase).ToList();
|
return GetBookDecisions(reports, false, searchCriteriaBase).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<DownloadDecision> GetBookDecisions(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteria = null)
|
private IEnumerable<DownloadDecision> GetBookDecisions(List<ReleaseInfo> reports, bool pushedRelease = false, SearchCriteriaBase searchCriteria = null)
|
||||||
{
|
{
|
||||||
if (reports.Any())
|
if (reports.Any())
|
||||||
{
|
{
|
||||||
@@ -206,6 +206,26 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
|
|
||||||
if (decision != null)
|
if (decision != null)
|
||||||
{
|
{
|
||||||
|
var source = pushedRelease ? ReleaseSourceType.ReleasePush : ReleaseSourceType.Rss;
|
||||||
|
|
||||||
|
if (searchCriteria != null)
|
||||||
|
{
|
||||||
|
if (searchCriteria.InteractiveSearch)
|
||||||
|
{
|
||||||
|
source = ReleaseSourceType.InteractiveSearch;
|
||||||
|
}
|
||||||
|
else if (searchCriteria.UserInvokedSearch)
|
||||||
|
{
|
||||||
|
source = ReleaseSourceType.UserInvokedSearch;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
source = ReleaseSourceType.Search;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decision.RemoteBook.ReleaseSource = source;
|
||||||
|
|
||||||
if (decision.Rejections.Any())
|
if (decision.Rejections.Any())
|
||||||
{
|
{
|
||||||
_logger.Debug("Release rejected for the following reasons: {0}", string.Join(", ", decision.Rejections));
|
_logger.Debug("Release rejected for the following reasons: {0}", string.Join(", ", decision.Rejections));
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
@@ -27,8 +28,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
using NzbDrone.Core.Organizer;
|
||||||
@@ -29,8 +30,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_scanWatchFolder = scanWatchFolder;
|
_scanWatchFolder = scanWatchFolder;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
@@ -25,8 +26,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
@@ -36,8 +37,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_dsInfoProxy = dsInfoProxy;
|
_dsInfoProxy = dsInfoProxy;
|
||||||
_dsTaskProxySelector = dsTaskProxySelector;
|
_dsTaskProxySelector = dsTaskProxySelector;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.Flood.Models;
|
using NzbDrone.Core.Download.Clients.Flood.Models;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
@@ -27,8 +28,9 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
_downloadSeedConfigProvider = downloadSeedConfigProvider;
|
_downloadSeedConfigProvider = downloadSeedConfigProvider;
|
||||||
@@ -70,7 +72,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result.Where(t => t.IsNotNullOrWhiteSpace());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Name => "Flood";
|
public override string Name => "Flood";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.Hadouken.Models;
|
using NzbDrone.Core.Download.Clients.Hadouken.Models;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
@@ -24,8 +25,9 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Cache;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
@@ -34,8 +35,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
ICacheManager cacheManager,
|
ICacheManager cacheManager,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxySelector = proxySelector;
|
_proxySelector = proxySelector;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
|
{
|
||||||
|
public enum QBittorrentContentLayout
|
||||||
|
{
|
||||||
|
Default = 0,
|
||||||
|
Original = 1,
|
||||||
|
Subfolder = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -265,6 +265,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
request.AddFormParameter("firstLastPiecePrio", true);
|
request.AddFormParameter("firstLastPiecePrio", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Original)
|
||||||
|
{
|
||||||
|
request.AddFormParameter("contentLayout", "Original");
|
||||||
|
}
|
||||||
|
else if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Subfolder)
|
||||||
|
{
|
||||||
|
request.AddFormParameter("contentLayout", "Subfolder");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
[FieldDefinition(12, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")]
|
[FieldDefinition(12, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")]
|
||||||
public bool FirstAndLast { get; set; }
|
public bool FirstAndLast { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")]
|
||||||
|
public int ContentLayout { get; set; }
|
||||||
|
|
||||||
public NzbDroneValidationResult Validate()
|
public NzbDroneValidationResult Validate()
|
||||||
{
|
{
|
||||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
@@ -18,8 +19,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
@@ -24,8 +25,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using FluentValidation.Results;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.Transmission;
|
using NzbDrone.Core.Download.Clients.Transmission;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
@@ -19,8 +20,9 @@ namespace NzbDrone.Core.Download.Clients.Vuze
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Disk;
|
|||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.rTorrent;
|
using NzbDrone.Core.Download.Clients.rTorrent;
|
||||||
using NzbDrone.Core.Exceptions;
|
using NzbDrone.Core.Exceptions;
|
||||||
@@ -34,8 +35,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
IDownloadSeedConfigProvider downloadSeedConfigProvider,
|
IDownloadSeedConfigProvider downloadSeedConfigProvider,
|
||||||
IRTorrentDirectoryValidator rTorrentDirectoryValidator,
|
IRTorrentDirectoryValidator rTorrentDirectoryValidator,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
_rTorrentDirectoryValidator = rTorrentDirectoryValidator;
|
_rTorrentDirectoryValidator = rTorrentDirectoryValidator;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Cache;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
@@ -28,8 +29,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
|||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
|
using Polly;
|
||||||
|
using Polly.Retry;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download
|
namespace NzbDrone.Core.Download
|
||||||
{
|
{
|
||||||
@@ -21,6 +25,37 @@ namespace NzbDrone.Core.Download
|
|||||||
protected readonly IRemotePathMappingService _remotePathMappingService;
|
protected readonly IRemotePathMappingService _remotePathMappingService;
|
||||||
protected readonly Logger _logger;
|
protected readonly Logger _logger;
|
||||||
|
|
||||||
|
protected ResiliencePipeline<HttpResponse> RetryStrategy => new ResiliencePipelineBuilder<HttpResponse>()
|
||||||
|
.AddRetry(new RetryStrategyOptions<HttpResponse>
|
||||||
|
{
|
||||||
|
ShouldHandle = static args => args.Outcome switch
|
||||||
|
{
|
||||||
|
{ Result.HasHttpServerError: true } => PredicateResult.True(),
|
||||||
|
{ Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(),
|
||||||
|
_ => PredicateResult.False()
|
||||||
|
},
|
||||||
|
Delay = TimeSpan.FromSeconds(3),
|
||||||
|
MaxRetryAttempts = 2,
|
||||||
|
BackoffType = DelayBackoffType.Exponential,
|
||||||
|
UseJitter = true,
|
||||||
|
OnRetry = args =>
|
||||||
|
{
|
||||||
|
var exception = args.Outcome.Exception;
|
||||||
|
|
||||||
|
if (exception is not null)
|
||||||
|
{
|
||||||
|
_logger.Info(exception, "Request for {0} failed with exception '{1}'. Retrying in {2}s.", Definition.Name, exception.Message, args.RetryDelay.TotalSeconds);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Info("Request for {0} failed with status {1}. Retrying in {2}s.", Definition.Name, args.Outcome.Result?.StatusCode, args.RetryDelay.TotalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
public abstract string Name { get; }
|
public abstract string Name { get; }
|
||||||
|
|
||||||
public Type ConfigContract => typeof(TSettings);
|
public Type ConfigContract => typeof(TSettings);
|
||||||
@@ -54,10 +89,7 @@ namespace NzbDrone.Core.Download
|
|||||||
return GetType().Name;
|
return GetType().Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract DownloadProtocol Protocol
|
public abstract DownloadProtocol Protocol { get; }
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Task<string> Download(RemoteBook remoteBook, IIndexer indexer);
|
public abstract Task<string> Download(RemoteBook remoteBook, IIndexer indexer);
|
||||||
public abstract IEnumerable<DownloadClientItem> GetItems();
|
public abstract IEnumerable<DownloadClientItem> GetItems();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NzbDrone.Common.Messaging;
|
using NzbDrone.Common.Messaging;
|
||||||
using NzbDrone.Core.Download.TrackedDownloads;
|
using NzbDrone.Core.Download.TrackedDownloads;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download
|
namespace NzbDrone.Core.Download
|
||||||
@@ -22,5 +23,6 @@ namespace NzbDrone.Core.Download
|
|||||||
public Dictionary<string, string> Data { get; set; }
|
public Dictionary<string, string> Data { get; set; }
|
||||||
public TrackedDownload TrackedDownload { get; set; }
|
public TrackedDownload TrackedDownload { get; set; }
|
||||||
public bool SkipRedownload { get; set; }
|
public bool SkipRedownload { get; set; }
|
||||||
|
public ReleaseSourceType ReleaseSource { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Download
|
|||||||
{
|
{
|
||||||
public interface IDownloadService
|
public interface IDownloadService
|
||||||
{
|
{
|
||||||
Task DownloadReport(RemoteBook remoteBook);
|
Task DownloadReport(RemoteBook remoteBook, int? downloadClientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DownloadService : IDownloadService
|
public class DownloadService : IDownloadService
|
||||||
@@ -50,13 +50,15 @@ namespace NzbDrone.Core.Download
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadReport(RemoteBook remoteBook)
|
public async Task DownloadReport(RemoteBook remoteBook, int? downloadClientId)
|
||||||
{
|
{
|
||||||
var filterBlockedClients = remoteBook.Release.PendingReleaseReason == PendingReleaseReason.DownloadClientUnavailable;
|
var filterBlockedClients = remoteBook.Release.PendingReleaseReason == PendingReleaseReason.DownloadClientUnavailable;
|
||||||
|
|
||||||
var tags = remoteBook.Author?.Tags;
|
var tags = remoteBook.Author?.Tags;
|
||||||
|
|
||||||
var downloadClient = _downloadClientProvider.GetDownloadClient(remoteBook.Release.DownloadProtocol, remoteBook.Release.IndexerId, filterBlockedClients, tags);
|
var downloadClient = downloadClientId.HasValue
|
||||||
|
? _downloadClientProvider.Get(downloadClientId.Value)
|
||||||
|
: _downloadClientProvider.GetDownloadClient(remoteBook.Release.DownloadProtocol, remoteBook.Release.IndexerId, filterBlockedClients, tags);
|
||||||
|
|
||||||
await DownloadReport(remoteBook, downloadClient);
|
await DownloadReport(remoteBook, downloadClient);
|
||||||
}
|
}
|
||||||
@@ -102,6 +104,11 @@ namespace NzbDrone.Core.Download
|
|||||||
_logger.Trace("Release {0} no longer available on indexer.", remoteBook);
|
_logger.Trace("Release {0} no longer available on indexer.", remoteBook);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
catch (ReleaseBlockedException)
|
||||||
|
{
|
||||||
|
_logger.Trace("Release {0} previously added to blocklist, not sending to download client again.", remoteBook);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (DownloadClientRejectedReleaseException)
|
catch (DownloadClientRejectedReleaseException)
|
||||||
{
|
{
|
||||||
_logger.Trace("Release {0} rejected by download client, possible duplicate.", remoteBook);
|
_logger.Trace("Release {0} rejected by download client, possible duplicate.", remoteBook);
|
||||||
@@ -126,7 +133,7 @@ namespace NzbDrone.Core.Download
|
|||||||
bookGrabbedEvent.DownloadClientId = downloadClient.Definition.Id;
|
bookGrabbedEvent.DownloadClientId = downloadClient.Definition.Id;
|
||||||
bookGrabbedEvent.DownloadClientName = downloadClient.Definition.Name;
|
bookGrabbedEvent.DownloadClientName = downloadClient.Definition.Name;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(downloadClientId))
|
if (downloadClientId.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
bookGrabbedEvent.DownloadId = downloadClientId;
|
bookGrabbedEvent.DownloadId = downloadClientId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Download.TrackedDownloads;
|
using NzbDrone.Core.Download.TrackedDownloads;
|
||||||
using NzbDrone.Core.History;
|
using NzbDrone.Core.History;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download
|
namespace NzbDrone.Core.Download
|
||||||
{
|
{
|
||||||
@@ -116,7 +118,8 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
private void PublishDownloadFailedEvent(List<EntityHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false)
|
private void PublishDownloadFailedEvent(List<EntityHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false)
|
||||||
{
|
{
|
||||||
var historyItem = historyItems.First();
|
var historyItem = historyItems.Last();
|
||||||
|
Enum.TryParse(historyItem.Data.GetValueOrDefault(EntityHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource);
|
||||||
|
|
||||||
var downloadFailedEvent = new DownloadFailedEvent
|
var downloadFailedEvent = new DownloadFailedEvent
|
||||||
{
|
{
|
||||||
@@ -129,7 +132,8 @@ namespace NzbDrone.Core.Download
|
|||||||
Message = message,
|
Message = message,
|
||||||
Data = historyItem.Data,
|
Data = historyItem.Data,
|
||||||
TrackedDownload = trackedDownload,
|
TrackedDownload = trackedDownload,
|
||||||
SkipRedownload = skipRedownload
|
SkipRedownload = skipRedownload,
|
||||||
|
ReleaseSource = releaseSource
|
||||||
};
|
};
|
||||||
|
|
||||||
_eventAggregator.PublishEvent(downloadFailedEvent);
|
_eventAggregator.PublishEvent(downloadFailedEvent);
|
||||||
|
|||||||
@@ -12,8 +12,14 @@ namespace NzbDrone.Core.Download.Pending
|
|||||||
public ParsedBookInfo ParsedBookInfo { get; set; }
|
public ParsedBookInfo ParsedBookInfo { get; set; }
|
||||||
public ReleaseInfo Release { get; set; }
|
public ReleaseInfo Release { get; set; }
|
||||||
public PendingReleaseReason Reason { get; set; }
|
public PendingReleaseReason Reason { get; set; }
|
||||||
|
public PendingReleaseAdditionalInfo AdditionalInfo { get; set; }
|
||||||
|
|
||||||
//Not persisted
|
//Not persisted
|
||||||
public RemoteBook RemoteBook { get; set; }
|
public RemoteBook RemoteBook { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PendingReleaseAdditionalInfo
|
||||||
|
{
|
||||||
|
public ReleaseSourceType ReleaseSource { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ namespace NzbDrone.Core.Download.Pending
|
|||||||
{
|
{
|
||||||
Author = author,
|
Author = author,
|
||||||
Books = books,
|
Books = books,
|
||||||
|
ReleaseSource = release.AdditionalInfo?.ReleaseSource ?? ReleaseSourceType.Unknown,
|
||||||
ParsedBookInfo = release.ParsedBookInfo,
|
ParsedBookInfo = release.ParsedBookInfo,
|
||||||
Release = release.Release
|
Release = release.Release
|
||||||
};
|
};
|
||||||
@@ -342,7 +343,11 @@ namespace NzbDrone.Core.Download.Pending
|
|||||||
Release = decision.RemoteBook.Release,
|
Release = decision.RemoteBook.Release,
|
||||||
Title = decision.RemoteBook.Release.Title,
|
Title = decision.RemoteBook.Release.Title,
|
||||||
Added = DateTime.UtcNow,
|
Added = DateTime.UtcNow,
|
||||||
Reason = reason
|
Reason = reason,
|
||||||
|
AdditionalInfo = new PendingReleaseAdditionalInfo
|
||||||
|
{
|
||||||
|
ReleaseSource = decision.RemoteBook.ReleaseSource
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent());
|
_eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent());
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace NzbDrone.Core.Download
|
|||||||
public interface IProcessDownloadDecisions
|
public interface IProcessDownloadDecisions
|
||||||
{
|
{
|
||||||
Task<ProcessedDecisions> ProcessDecisions(List<DownloadDecision> decisions);
|
Task<ProcessedDecisions> ProcessDecisions(List<DownloadDecision> decisions);
|
||||||
|
Task<ProcessedDecisionResult> ProcessDecision(DownloadDecision decision, int? downloadClientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ProcessDownloadDecisions : IProcessDownloadDecisions
|
public class ProcessDownloadDecisions : IProcessDownloadDecisions
|
||||||
@@ -40,8 +41,6 @@ namespace NzbDrone.Core.Download
|
|||||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports);
|
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports);
|
||||||
var grabbed = new List<DownloadDecision>();
|
var grabbed = new List<DownloadDecision>();
|
||||||
var pending = new List<DownloadDecision>();
|
var pending = new List<DownloadDecision>();
|
||||||
|
|
||||||
//var failed = new List<DownloadDecision>();
|
|
||||||
var rejected = decisions.Where(d => d.Rejected).ToList();
|
var rejected = decisions.Where(d => d.Rejected).ToList();
|
||||||
|
|
||||||
var pendingAddQueue = new List<Tuple<DownloadDecision, PendingReleaseReason>>();
|
var pendingAddQueue = new List<Tuple<DownloadDecision, PendingReleaseReason>>();
|
||||||
@@ -51,7 +50,6 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
foreach (var report in prioritizedDecisions)
|
foreach (var report in prioritizedDecisions)
|
||||||
{
|
{
|
||||||
var remoteBook = report.RemoteBook;
|
|
||||||
var downloadProtocol = report.RemoteBook.Release.DownloadProtocol;
|
var downloadProtocol = report.RemoteBook.Release.DownloadProtocol;
|
||||||
|
|
||||||
//Skip if already grabbed
|
//Skip if already grabbed
|
||||||
@@ -73,37 +71,48 @@ namespace NzbDrone.Core.Download
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
var result = await ProcessDecisionInternal(report);
|
||||||
{
|
|
||||||
_logger.Trace("Grabbing from Indexer {0} at priority {1}.", remoteBook.Release.Indexer, remoteBook.Release.IndexerPriority);
|
|
||||||
await _downloadService.DownloadReport(remoteBook);
|
|
||||||
grabbed.Add(report);
|
|
||||||
}
|
|
||||||
catch (ReleaseUnavailableException)
|
|
||||||
{
|
|
||||||
_logger.Warn("Failed to download release from indexer, no longer available. " + remoteBook);
|
|
||||||
rejected.Add(report);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (ex is DownloadClientUnavailableException || ex is DownloadClientAuthenticationException)
|
|
||||||
{
|
|
||||||
_logger.Debug(ex, "Failed to send release to download client, storing until later. " + remoteBook);
|
|
||||||
PreparePending(pendingAddQueue, grabbed, pending, report, PendingReleaseReason.DownloadClientUnavailable);
|
|
||||||
|
|
||||||
if (downloadProtocol == DownloadProtocol.Usenet)
|
switch (result)
|
||||||
|
{
|
||||||
|
case ProcessedDecisionResult.Grabbed:
|
||||||
{
|
{
|
||||||
usenetFailed = true;
|
grabbed.Add(report);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else if (downloadProtocol == DownloadProtocol.Torrent)
|
|
||||||
|
case ProcessedDecisionResult.Pending:
|
||||||
{
|
{
|
||||||
torrentFailed = true;
|
PreparePending(pendingAddQueue, grabbed, pending, report, PendingReleaseReason.Delay);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ProcessedDecisionResult.Rejected:
|
||||||
|
{
|
||||||
|
rejected.Add(report);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ProcessedDecisionResult.Failed:
|
||||||
|
{
|
||||||
|
PreparePending(pendingAddQueue, grabbed, pending, report, PendingReleaseReason.DownloadClientUnavailable);
|
||||||
|
|
||||||
|
if (downloadProtocol == DownloadProtocol.Usenet)
|
||||||
|
{
|
||||||
|
usenetFailed = true;
|
||||||
|
}
|
||||||
|
else if (downloadProtocol == DownloadProtocol.Torrent)
|
||||||
|
{
|
||||||
|
torrentFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ProcessedDecisionResult.Skipped:
|
||||||
|
{
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.Warn(ex, "Couldn't add report to download queue. " + remoteBook);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,10 +124,44 @@ namespace NzbDrone.Core.Download
|
|||||||
return new ProcessedDecisions(grabbed, pending, rejected);
|
return new ProcessedDecisions(grabbed, pending, rejected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ProcessedDecisionResult> ProcessDecision(DownloadDecision decision, int? downloadClientId)
|
||||||
|
{
|
||||||
|
if (decision == null)
|
||||||
|
{
|
||||||
|
return ProcessedDecisionResult.Skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsQualifiedReport(decision))
|
||||||
|
{
|
||||||
|
return ProcessedDecisionResult.Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decision.TemporarilyRejected)
|
||||||
|
{
|
||||||
|
_pendingReleaseService.Add(decision, PendingReleaseReason.Delay);
|
||||||
|
|
||||||
|
return ProcessedDecisionResult.Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await ProcessDecisionInternal(decision, downloadClientId);
|
||||||
|
|
||||||
|
if (result == ProcessedDecisionResult.Failed)
|
||||||
|
{
|
||||||
|
_pendingReleaseService.Add(decision, PendingReleaseReason.DownloadClientUnavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
internal List<DownloadDecision> GetQualifiedReports(IEnumerable<DownloadDecision> decisions)
|
internal List<DownloadDecision> GetQualifiedReports(IEnumerable<DownloadDecision> decisions)
|
||||||
{
|
{
|
||||||
//Process both approved and temporarily rejected
|
return decisions.Where(IsQualifiedReport).ToList();
|
||||||
return decisions.Where(c => (c.Approved || c.TemporarilyRejected) && c.RemoteBook.Books.Any()).ToList();
|
}
|
||||||
|
|
||||||
|
internal bool IsQualifiedReport(DownloadDecision decision)
|
||||||
|
{
|
||||||
|
// Process both approved and temporarily rejected
|
||||||
|
return (decision.Approved || decision.TemporarilyRejected) && decision.RemoteBook.Books.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsBookProcessed(List<DownloadDecision> decisions, DownloadDecision report)
|
private bool IsBookProcessed(List<DownloadDecision> decisions, DownloadDecision report)
|
||||||
@@ -148,5 +191,38 @@ namespace NzbDrone.Core.Download
|
|||||||
queue.Add(Tuple.Create(report, reason));
|
queue.Add(Tuple.Create(report, reason));
|
||||||
pending.Add(report);
|
pending.Add(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ProcessedDecisionResult> ProcessDecisionInternal(DownloadDecision decision, int? downloadClientId = null)
|
||||||
|
{
|
||||||
|
var remoteBook = decision.RemoteBook;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Trace("Grabbing from Indexer {0} at priority {1}.", remoteBook.Release.Indexer, remoteBook.Release.IndexerPriority);
|
||||||
|
await _downloadService.DownloadReport(remoteBook, downloadClientId);
|
||||||
|
|
||||||
|
return ProcessedDecisionResult.Grabbed;
|
||||||
|
}
|
||||||
|
catch (ReleaseUnavailableException)
|
||||||
|
{
|
||||||
|
_logger.Warn("Failed to download release from indexer, no longer available. " + remoteBook);
|
||||||
|
return ProcessedDecisionResult.Rejected;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (ex is DownloadClientUnavailableException || ex is DownloadClientAuthenticationException)
|
||||||
|
{
|
||||||
|
_logger.Debug(ex,
|
||||||
|
"Failed to send release to download client, storing until later. " + remoteBook);
|
||||||
|
|
||||||
|
return ProcessedDecisionResult.Failed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Warn(ex, "Couldn't add report to download queue. " + remoteBook);
|
||||||
|
return ProcessedDecisionResult.Skipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace NzbDrone.Core.Download
|
||||||
|
{
|
||||||
|
public enum ProcessedDecisionResult
|
||||||
|
{
|
||||||
|
Grabbed,
|
||||||
|
Pending,
|
||||||
|
Rejected,
|
||||||
|
Failed,
|
||||||
|
Skipped
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using NzbDrone.Core.IndexerSearch;
|
|||||||
using NzbDrone.Core.Messaging;
|
using NzbDrone.Core.Messaging;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download
|
namespace NzbDrone.Core.Download
|
||||||
{
|
{
|
||||||
@@ -41,6 +42,12 @@ namespace NzbDrone.Core.Download
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.ReleaseSource == ReleaseSourceType.InteractiveSearch && !_configService.AutoRedownloadFailedFromInteractiveSearch)
|
||||||
|
{
|
||||||
|
_logger.Debug("Auto redownloading failed books from interactive search is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.BookIds.Count == 1)
|
if (message.BookIds.Count == 1)
|
||||||
{
|
{
|
||||||
_logger.Debug("Failed download only contains one book, searching again");
|
_logger.Debug("Failed download only contains one book, searching again");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Exceptions;
|
using NzbDrone.Core.Exceptions;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
@@ -21,17 +22,20 @@ namespace NzbDrone.Core.Download
|
|||||||
where TSettings : IProviderConfig, new()
|
where TSettings : IProviderConfig, new()
|
||||||
{
|
{
|
||||||
protected readonly IHttpClient _httpClient;
|
protected readonly IHttpClient _httpClient;
|
||||||
|
private readonly IBlocklistService _blocklistService;
|
||||||
protected readonly ITorrentFileInfoReader _torrentFileInfoReader;
|
protected readonly ITorrentFileInfoReader _torrentFileInfoReader;
|
||||||
|
|
||||||
protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader,
|
protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
Logger logger)
|
IBlocklistService blocklistService,
|
||||||
|
Logger logger)
|
||||||
: base(configService, diskProvider, remotePathMappingService, logger)
|
: base(configService, diskProvider, remotePathMappingService, logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
|
_blocklistService = blocklistService;
|
||||||
_torrentFileInfoReader = torrentFileInfoReader;
|
_torrentFileInfoReader = torrentFileInfoReader;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +90,7 @@ namespace NzbDrone.Core.Download
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return DownloadFromMagnetUrl(remoteBook, magnetUrl);
|
return DownloadFromMagnetUrl(remoteBook, indexer, magnetUrl);
|
||||||
}
|
}
|
||||||
catch (NotSupportedException ex)
|
catch (NotSupportedException ex)
|
||||||
{
|
{
|
||||||
@@ -100,7 +104,7 @@ namespace NzbDrone.Core.Download
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return DownloadFromMagnetUrl(remoteBook, magnetUrl);
|
return DownloadFromMagnetUrl(remoteBook, indexer, magnetUrl);
|
||||||
}
|
}
|
||||||
catch (NotSupportedException ex)
|
catch (NotSupportedException ex)
|
||||||
{
|
{
|
||||||
@@ -133,7 +137,9 @@ namespace NzbDrone.Core.Download
|
|||||||
request.Headers.Accept = "application/x-bittorrent";
|
request.Headers.Accept = "application/x-bittorrent";
|
||||||
request.AllowAutoRedirect = false;
|
request.AllowAutoRedirect = false;
|
||||||
|
|
||||||
var response = await _httpClient.GetAsync(request);
|
var response = await RetryStrategy
|
||||||
|
.ExecuteAsync(static async (state, _) => await state._httpClient.GetAsync(state.request), (_httpClient, request))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.MovedPermanently ||
|
if (response.StatusCode == HttpStatusCode.MovedPermanently ||
|
||||||
response.StatusCode == HttpStatusCode.Found ||
|
response.StatusCode == HttpStatusCode.Found ||
|
||||||
@@ -147,7 +153,7 @@ namespace NzbDrone.Core.Download
|
|||||||
{
|
{
|
||||||
if (locationHeader.StartsWith("magnet:"))
|
if (locationHeader.StartsWith("magnet:"))
|
||||||
{
|
{
|
||||||
return DownloadFromMagnetUrl(remoteBook, locationHeader);
|
return DownloadFromMagnetUrl(remoteBook, indexer, locationHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Url += new HttpUri(locationHeader);
|
request.Url += new HttpUri(locationHeader);
|
||||||
@@ -190,6 +196,9 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteBook.Release.Title));
|
var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteBook.Release.Title));
|
||||||
var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile);
|
var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile);
|
||||||
|
|
||||||
|
EnsureReleaseIsNotBlocklisted(remoteBook, indexer, hash);
|
||||||
|
|
||||||
var actualHash = AddFromTorrentFile(remoteBook, hash, filename, torrentFile);
|
var actualHash = AddFromTorrentFile(remoteBook, hash, filename, torrentFile);
|
||||||
|
|
||||||
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
|
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
|
||||||
@@ -203,7 +212,7 @@ namespace NzbDrone.Core.Download
|
|||||||
return actualHash;
|
return actualHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string DownloadFromMagnetUrl(RemoteBook remoteBook, string magnetUrl)
|
private string DownloadFromMagnetUrl(RemoteBook remoteBook, IIndexer indexer, string magnetUrl)
|
||||||
{
|
{
|
||||||
string hash = null;
|
string hash = null;
|
||||||
string actualHash = null;
|
string actualHash = null;
|
||||||
@@ -221,6 +230,8 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
if (hash != null)
|
if (hash != null)
|
||||||
{
|
{
|
||||||
|
EnsureReleaseIsNotBlocklisted(remoteBook, indexer, hash);
|
||||||
|
|
||||||
actualHash = AddFromMagnetLink(remoteBook, hash, magnetUrl);
|
actualHash = AddFromMagnetLink(remoteBook, hash, magnetUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,5 +245,29 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
return actualHash;
|
return actualHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureReleaseIsNotBlocklisted(RemoteBook remoteBook, IIndexer indexer, string hash)
|
||||||
|
{
|
||||||
|
var indexerSettings = indexer?.Definition?.Settings as ITorrentIndexerSettings;
|
||||||
|
var torrentInfo = remoteBook.Release as TorrentInfo;
|
||||||
|
var torrentInfoHash = torrentInfo?.InfoHash;
|
||||||
|
|
||||||
|
// If the release didn't come from an interactive search,
|
||||||
|
// the hash wasn't known during processing and the
|
||||||
|
// indexer is configured to reject blocklisted releases
|
||||||
|
// during grab check if it's already been blocklisted.
|
||||||
|
if (torrentInfo != null && torrentInfoHash.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
// If the hash isn't known from parsing we set it here so it can be used for blocklisting.
|
||||||
|
torrentInfo.InfoHash = hash;
|
||||||
|
|
||||||
|
if (remoteBook.ReleaseSource != ReleaseSourceType.InteractiveSearch &&
|
||||||
|
indexerSettings?.RejectBlocklistedTorrentHashesWhileGrabbing == true &&
|
||||||
|
_blocklistService.BlocklistedTorrentHash(remoteBook.Author.Id, hash))
|
||||||
|
{
|
||||||
|
throw new ReleaseBlockedException(remoteBook.Release, "Release previously added to blocklist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ namespace NzbDrone.Core.Download
|
|||||||
var request = indexer?.GetDownloadRequest(url) ?? new HttpRequest(url);
|
var request = indexer?.GetDownloadRequest(url) ?? new HttpRequest(url);
|
||||||
request.RateLimitKey = remoteBook?.Release?.IndexerId.ToString();
|
request.RateLimitKey = remoteBook?.Release?.IndexerId.ToString();
|
||||||
|
|
||||||
var response = await _httpClient.GetAsync(request);
|
var response = await RetryStrategy
|
||||||
|
.ExecuteAsync(static async (state, _) => await state._httpClient.GetAsync(state.request), (_httpClient, request))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
nzbData = response.ResponseData;
|
nzbData = response.ResponseData;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Exceptions
|
||||||
|
{
|
||||||
|
public class ReleaseBlockedException : ReleaseDownloadException
|
||||||
|
{
|
||||||
|
public ReleaseBlockedException(ReleaseInfo release, string message, params object[] args)
|
||||||
|
: base(release, message, args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReleaseBlockedException(ReleaseInfo release, string message)
|
||||||
|
: base(release, message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReleaseBlockedException(ReleaseInfo release, string message, Exception innerException, params object[] args)
|
||||||
|
: base(release, message, innerException, args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReleaseBlockedException(ReleaseInfo release, string message, Exception innerException)
|
||||||
|
: base(release, message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.History
|
|||||||
public class EntityHistory : ModelBase
|
public class EntityHistory : ModelBase
|
||||||
{
|
{
|
||||||
public const string DOWNLOAD_CLIENT = "downloadClient";
|
public const string DOWNLOAD_CLIENT = "downloadClient";
|
||||||
|
public const string RELEASE_SOURCE = "releaseSource";
|
||||||
|
|
||||||
public EntityHistory()
|
public EntityHistory()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -162,15 +162,15 @@ namespace NzbDrone.Core.History
|
|||||||
history.Data.Add("Guid", message.Book.Release.Guid);
|
history.Data.Add("Guid", message.Book.Release.Guid);
|
||||||
history.Data.Add("Protocol", ((int)message.Book.Release.DownloadProtocol).ToString());
|
history.Data.Add("Protocol", ((int)message.Book.Release.DownloadProtocol).ToString());
|
||||||
history.Data.Add("DownloadForced", (!message.Book.DownloadAllowed).ToString());
|
history.Data.Add("DownloadForced", (!message.Book.DownloadAllowed).ToString());
|
||||||
|
history.Data.Add("CustomFormatScore", message.Book.CustomFormatScore.ToString());
|
||||||
|
history.Data.Add("ReleaseSource", message.Book.ReleaseSource.ToString());
|
||||||
|
|
||||||
if (!message.Book.ParsedBookInfo.ReleaseHash.IsNullOrWhiteSpace())
|
if (!message.Book.ParsedBookInfo.ReleaseHash.IsNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
history.Data.Add("ReleaseHash", message.Book.ParsedBookInfo.ReleaseHash);
|
history.Data.Add("ReleaseHash", message.Book.ParsedBookInfo.ReleaseHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
var torrentRelease = message.Book.Release as TorrentInfo;
|
if (message.Book.Release is TorrentInfo torrentRelease)
|
||||||
|
|
||||||
if (torrentRelease != null)
|
|
||||||
{
|
{
|
||||||
history.Data.Add("TorrentInfoHash", torrentRelease.InfoHash);
|
history.Data.Add("TorrentInfoHash", torrentRelease.InfoHash);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace NzbDrone.Core.ImportLists.Readarr
|
|||||||
{
|
{
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string ForeignBookId { get; set; }
|
public string ForeignBookId { get; set; }
|
||||||
|
public string ForeignEditionId { get; set; }
|
||||||
public string Overview { get; set; }
|
public string Overview { get; set; }
|
||||||
public List<MediaCover.MediaCover> Images { get; set; }
|
public List<MediaCover.MediaCover> Images { get; set; }
|
||||||
public bool Monitored { get; set; }
|
public bool Monitored { get; set; }
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user