mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-26 17:44:27 -04:00
Compare commits
87 Commits
v0.3.14.23
...
v0.3.19.24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c77d820763 | ||
|
|
3327ed0f49 | ||
|
|
44009e980b | ||
|
|
02fd733223 | ||
|
|
2fa9576d05 | ||
|
|
c7ee278ee4 | ||
|
|
d72c27ceed | ||
|
|
7a20fe2288 | ||
|
|
042b62a2a5 | ||
|
|
88141e9d63 | ||
|
|
7fa1114edf | ||
|
|
d4262532e2 | ||
|
|
a21f83aae1 | ||
|
|
d659e86a7d | ||
|
|
0b924005ec | ||
|
|
ba2fad5d9c | ||
|
|
58416cee67 | ||
|
|
38124313c7 | ||
|
|
3fc9f6c0a4 | ||
|
|
79ce5abd53 | ||
|
|
7f01d597cb | ||
|
|
31f35df71d | ||
|
|
faeb78801c | ||
|
|
bd5695f2dd | ||
|
|
5375cbe1c2 | ||
|
|
d0b797ea61 | ||
|
|
e76f160695 | ||
|
|
ef71fc1b41 | ||
|
|
14f14e5da4 | ||
|
|
bd265e47fa | ||
|
|
333d344c0b | ||
|
|
db6712f030 | ||
|
|
1065a6283c | ||
|
|
1b40c5c7ce | ||
|
|
a8de87300e | ||
|
|
f260078ac8 | ||
|
|
5a6486be21 | ||
|
|
2e9de3cb86 | ||
|
|
a259684916 | ||
|
|
5704adfbc5 | ||
|
|
6cfaab07ba | ||
|
|
b36085a3cc | ||
|
|
0afa0977b0 | ||
|
|
4a174e559f | ||
|
|
0fb8ab2280 | ||
|
|
261b0f398b | ||
|
|
d1fea384a7 | ||
|
|
9542ea0d2e | ||
|
|
e1d697c561 | ||
|
|
22ed847849 | ||
|
|
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 |
16
.github/label-actions.yml
vendored
Normal file
16
.github/label-actions.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Configuration for Label Actions - https://github.com/dessant/label-actions
|
||||
|
||||
'Type: Support':
|
||||
comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
|
||||
close: true
|
||||
close-reason: 'not planned'
|
||||
|
||||
'Status: Logs Needed':
|
||||
comment: >
|
||||
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||
Guidance on how to enable trace logging can be found in
|
||||
our [troubleshooting guide](https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files).
|
||||
17
.github/workflows/label-actions.yml
vendored
Normal file
17
.github/workflows/label-actions.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: 'Label Actions'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v3
|
||||
with:
|
||||
process-only: 'issues'
|
||||
31
.github/workflows/support.yml
vendored
31
.github/workflows/support.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: 'Support requests'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
jobs:
|
||||
support:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Type: Support'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
- uses: dessant/support-requests@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Status: Logs Needed'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||
Guidance on how to enable trace logging can be found in
|
||||
our [troubleshooting guide](https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files).
|
||||
close-issue: false
|
||||
lock-issue: false
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.3.14'
|
||||
majorVersion: '0.3.19'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
|
||||
@@ -2,6 +2,8 @@ const loose = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||
|
||||
@@ -23,7 +23,7 @@ import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
@@ -289,9 +289,16 @@ class Queue extends Component {
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
)}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
@@ -299,7 +306,7 @@ class Queue extends Component {
|
||||
return !!(item && item.authorId && item.bookId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ class QueueRow extends Component {
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
downloadForced,
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
@@ -389,6 +390,7 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!author}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
@@ -418,6 +420,7 @@ QueueRow.propTypes = {
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
downloadForced: PropTypes.bool.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Remove - {sourceTitle}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
interface RemovePressProps {
|
||||
remove: boolean;
|
||||
changeCategory: boolean;
|
||||
blocklist: boolean;
|
||||
skipRedownload: boolean;
|
||||
}
|
||||
|
||||
interface RemoveQueueItemModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
canChangeCategory: boolean;
|
||||
canIgnore: boolean;
|
||||
isPending: boolean;
|
||||
selectedCount?: number;
|
||||
onRemovePress(props: RemovePressProps): void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||
type BlocklistMethod =
|
||||
| 'doNotBlocklist'
|
||||
| 'blocklistAndSearch'
|
||||
| 'blocklistOnly';
|
||||
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
canChangeCategory,
|
||||
isPending,
|
||||
selectedCount,
|
||||
onRemovePress,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const multipleSelected = selectedCount && selectedCount > 1;
|
||||
|
||||
const [removalMethod, setRemovalMethod] =
|
||||
useState<RemovalMethod>('removeFromClient');
|
||||
const [blocklistMethod, setBlocklistMethod] =
|
||||
useState<BlocklistMethod>('doNotBlocklist');
|
||||
|
||||
const { title, message } = useMemo(() => {
|
||||
if (!selectedCount) {
|
||||
return {
|
||||
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedCount === 1) {
|
||||
return {
|
||||
title: translate('RemoveSelectedItem'),
|
||||
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: translate('RemoveSelectedItems'),
|
||||
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||
selectedCount,
|
||||
}),
|
||||
};
|
||||
}, [sourceTitle, selectedCount]);
|
||||
|
||||
const removalMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'removeFromClient',
|
||||
value: translate('RemoveFromDownloadClient'),
|
||||
hint: multipleSelected
|
||||
? translate('RemoveMultipleFromDownloadClientHint')
|
||||
: translate('RemoveFromDownloadClientHint'),
|
||||
},
|
||||
{
|
||||
key: 'changeCategory',
|
||||
value: translate('ChangeCategory'),
|
||||
isDisabled: !canChangeCategory,
|
||||
hint: multipleSelected
|
||||
? translate('ChangeCategoryMultipleHint')
|
||||
: translate('ChangeCategoryHint'),
|
||||
},
|
||||
{
|
||||
key: 'ignore',
|
||||
value: multipleSelected
|
||||
? translate('IgnoreDownloads')
|
||||
: translate('IgnoreDownload'),
|
||||
isDisabled: !canIgnore,
|
||||
hint: multipleSelected
|
||||
? translate('IgnoreDownloadsHint')
|
||||
: translate('IgnoreDownloadHint'),
|
||||
},
|
||||
];
|
||||
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||
|
||||
const blocklistMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'doNotBlocklist',
|
||||
value: translate('DoNotBlocklist'),
|
||||
hint: translate('DoNotBlocklistHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistAndSearch',
|
||||
value: translate('BlocklistAndSearch'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistAndSearchMultipleHint')
|
||||
: translate('BlocklistAndSearchHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistOnly',
|
||||
value: translate('BlocklistOnly'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistMultipleOnlyHint')
|
||||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
}, [multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
setRemovalMethod(value);
|
||||
},
|
||||
[setRemovalMethod]
|
||||
);
|
||||
|
||||
const handleBlocklistMethodChange = useCallback(
|
||||
({ value }: { value: BlocklistMethod }) => {
|
||||
setBlocklistMethod(value);
|
||||
},
|
||||
[setBlocklistMethod]
|
||||
);
|
||||
|
||||
const handleConfirmRemove = useCallback(() => {
|
||||
onRemovePress({
|
||||
remove: removalMethod === 'removeFromClient',
|
||||
changeCategory: removalMethod === 'changeCategory',
|
||||
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||
});
|
||||
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
}, [
|
||||
removalMethod,
|
||||
blocklistMethod,
|
||||
setRemovalMethod,
|
||||
setBlocklistMethod,
|
||||
onRemovePress,
|
||||
]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
|
||||
onModalClose();
|
||||
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||
<ModalContent onModalClose={handleModalClose}>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>{message}</div>
|
||||
|
||||
{isPending ? null : (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="removalMethod"
|
||||
value={removalMethod}
|
||||
values={removalMethodOptions}
|
||||
isDisabled={!canChangeCategory && !canIgnore}
|
||||
helpTextWarning={translate(
|
||||
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||
)}
|
||||
onChange={handleRemovalMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{multipleSelected
|
||||
? translate('BlocklistReleases')
|
||||
: translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="blocklistMethod"
|
||||
value={blocklistMethod}
|
||||
values={blocklistMethodOptions}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={handleBlocklistMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
@@ -1,178 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemsModal.css';
|
||||
|
||||
class RemoveQueueItemsModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
</div>
|
||||
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
||||
@@ -155,7 +155,6 @@ function createMapStateToProps() {
|
||||
const isRefreshing = isAuthorRefreshing || allAuthorRefreshing;
|
||||
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 isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||
const isRenamingAuthor = (
|
||||
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||
|
||||
@@ -90,7 +90,7 @@ class AuthorIndexOverview extends Component {
|
||||
status,
|
||||
titleSlug,
|
||||
nextAiring,
|
||||
statistics,
|
||||
statistics = {},
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
@@ -113,10 +113,11 @@ class AuthorIndexOverview extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount,
|
||||
sizeOnDisk,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
bookCount = 0,
|
||||
availableBookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -179,6 +180,7 @@ class AuthorIndexOverview extends Component {
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
bookCount={bookCount}
|
||||
availableBookCount={availableBookCount}
|
||||
bookFileCount={bookFileCount}
|
||||
totalBookCount={totalBookCount}
|
||||
posterWidth={posterWidth}
|
||||
|
||||
@@ -85,7 +85,7 @@ class AuthorIndexPoster extends Component {
|
||||
titleSlug,
|
||||
status,
|
||||
nextAiring,
|
||||
statistics,
|
||||
statistics = {},
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
@@ -110,10 +110,11 @@ class AuthorIndexPoster extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount,
|
||||
sizeOnDisk,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
bookCount = 0,
|
||||
availableBookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -213,6 +214,7 @@ class AuthorIndexPoster extends Component {
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
bookCount={bookCount}
|
||||
availableBookCount={availableBookCount}
|
||||
bookFileCount={bookFileCount}
|
||||
totalBookCount={totalBookCount}
|
||||
posterWidth={posterWidth}
|
||||
|
||||
@@ -11,14 +11,15 @@ function AuthorIndexProgressBar(props) {
|
||||
monitored,
|
||||
status,
|
||||
bookCount,
|
||||
availableBookCount,
|
||||
bookFileCount,
|
||||
totalBookCount,
|
||||
posterWidth,
|
||||
detailedProgressBar
|
||||
} = props;
|
||||
|
||||
const progress = bookCount ? bookCount / totalBookCount * 100 : 100;
|
||||
const text = `${bookCount} / ${totalBookCount}`;
|
||||
const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
|
||||
const text = `${availableBookCount} / ${bookCount}`;
|
||||
|
||||
return (
|
||||
<ProgressBar
|
||||
@@ -29,7 +30,7 @@ function AuthorIndexProgressBar(props) {
|
||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||
showText={detailedProgressBar}
|
||||
text={text}
|
||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||
title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
|
||||
width={posterWidth}
|
||||
/>
|
||||
);
|
||||
@@ -39,6 +40,7 @@ AuthorIndexProgressBar.propTypes = {
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
bookCount: PropTypes.number.isRequired,
|
||||
availableBookCount: PropTypes.number.isRequired,
|
||||
bookFileCount: PropTypes.number.isRequired,
|
||||
totalBookCount: PropTypes.number.isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
|
||||
@@ -90,7 +90,7 @@ class AuthorIndexRow extends Component {
|
||||
nextBook,
|
||||
lastBook,
|
||||
added,
|
||||
statistics,
|
||||
statistics = {},
|
||||
genres,
|
||||
ratings,
|
||||
path,
|
||||
@@ -110,10 +110,11 @@ class AuthorIndexRow extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount,
|
||||
bookFileCount,
|
||||
totalBookCount,
|
||||
sizeOnDisk
|
||||
bookCount = 0,
|
||||
availableBookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -286,7 +287,7 @@ class AuthorIndexRow extends Component {
|
||||
}
|
||||
|
||||
if (name === 'bookProgress') {
|
||||
const progress = bookCount ? bookFileCount / bookCount * 100 : 100;
|
||||
const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
@@ -297,8 +298,8 @@ class AuthorIndexRow extends Component {
|
||||
progress={progress}
|
||||
kind={getProgressBarKind(status, monitored, progress)}
|
||||
showText={true}
|
||||
text={`${bookCount} / ${totalBookCount}`}
|
||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||
text={`${availableBookCount} / ${bookCount}`}
|
||||
title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
|
||||
width={125}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
@@ -69,16 +69,21 @@ function createMapStateToProps() {
|
||||
|
||||
const previousBook = sortedBooks[bookIndex - 1] || _.last(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 isSearching = (
|
||||
isCommandExecuting(isSearchingCommand) &&
|
||||
isSearchingCommand.body.bookIds.indexOf(book.id) > -1
|
||||
);
|
||||
|
||||
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
||||
const isRefreshing = (
|
||||
isCommandExecuting(isRefreshingCommand) &&
|
||||
isRefreshingCommand.body.bookId === book.id
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
||||
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||
const isRenamingAuthor = (
|
||||
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||
isRenamingAuthorCommand.body.authorIds.indexOf(author.id) > -1
|
||||
);
|
||||
|
||||
const isFetching = isBookFilesFetching || editions.isFetching;
|
||||
@@ -90,6 +95,8 @@ function createMapStateToProps() {
|
||||
author,
|
||||
isRefreshing,
|
||||
isSearching,
|
||||
isRenamingFiles,
|
||||
isRenamingAuthor,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
bookFilesError,
|
||||
@@ -125,9 +132,27 @@ class BookDetailsConnector extends Component {
|
||||
}
|
||||
|
||||
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)) ||
|
||||
(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.populate();
|
||||
}
|
||||
@@ -197,6 +222,8 @@ class BookDetailsConnector extends Component {
|
||||
BookDetailsConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
anyReleaseOk: PropTypes.bool,
|
||||
isRenamingFiles: PropTypes.bool.isRequired,
|
||||
isRenamingAuthor: PropTypes.bool.isRequired,
|
||||
isBookFetching: PropTypes.bool,
|
||||
isBookPopulated: PropTypes.bool,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
|
||||
@@ -16,8 +16,8 @@ function BookIndexProgressBar(props) {
|
||||
detailedProgressBar
|
||||
} = props;
|
||||
|
||||
const progress = bookCount ? bookFileCount / totalBookCount * 100 : 0;
|
||||
const text = `${bookFileCount} / ${bookCount}`;
|
||||
const progress = bookFileCount && bookCount ? (totalBookCount / bookCount) * 100 : 0;
|
||||
const text = `${bookFileCount ? bookCount : 0} / ${totalBookCount}`;
|
||||
|
||||
return (
|
||||
<ProgressBar
|
||||
@@ -28,7 +28,11 @@ function BookIndexProgressBar(props) {
|
||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||
showText={detailedProgressBar}
|
||||
text={text}
|
||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||
title={translate('BookProgressBarText', {
|
||||
bookCount: bookFileCount ? bookCount : 0,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
})}
|
||||
width={posterWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -27,14 +27,15 @@ class BookshelfBook extends Component {
|
||||
title,
|
||||
disambiguation,
|
||||
monitored,
|
||||
statistics,
|
||||
statistics = {},
|
||||
isSaving
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookFileCount,
|
||||
totalBookCount,
|
||||
percentOfBooks
|
||||
bookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
percentOfBooks = 0
|
||||
} = statistics;
|
||||
|
||||
return (
|
||||
@@ -59,10 +60,14 @@ class BookshelfBook extends Component {
|
||||
percentOfBooks < 100 && monitored && styles.missingWanted,
|
||||
percentOfBooks === 100 && styles.allBooks
|
||||
)}
|
||||
title={translate('BookFileCounttotalBookCountBooksDownloadedInterp', [bookFileCount, totalBookCount])}
|
||||
title={translate('BookProgressBarText', {
|
||||
bookCount: bookFileCount ? bookCount : 0,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
})}
|
||||
>
|
||||
{
|
||||
totalBookCount === 0 ? '0/0' : `${bookFileCount}/${totalBookCount}`
|
||||
totalBookCount === 0 ? '0/0' : `${bookFileCount ? bookCount : 0}/${totalBookCount}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,22 +29,24 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
|
||||
@@ -25,7 +25,8 @@ function createMapStateToProps() {
|
||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||
return {
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name
|
||||
value: downloadClient.name,
|
||||
hint: `(${downloadClient.id})`
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -273,6 +273,7 @@ FormInputGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
values: PropTypes.arrayOf(PropTypes.any),
|
||||
isDisabled: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
min: PropTypes.number,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: $formLabelRightMarginWidth;
|
||||
padding-top: 8px;
|
||||
min-height: 35px;
|
||||
text-align: end;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
|
||||
@@ -39,18 +39,26 @@ class FilterMenuContent extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
customFilters.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
customFilters.length > 0 ?
|
||||
<MenuItemSeparator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ class SelectAuthorModalContent extends Component {
|
||||
// Listeners
|
||||
|
||||
onFilterChange = ({ value }) => {
|
||||
this.setState({ filter: value.toLowerCase() });
|
||||
this.setState({ filter: value });
|
||||
};
|
||||
|
||||
//
|
||||
@@ -43,6 +43,7 @@ class SelectAuthorModalContent extends Component {
|
||||
} = this.props;
|
||||
|
||||
const filter = this.state.filter;
|
||||
const filterLower = filter.toLowerCase();
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -66,7 +67,7 @@ class SelectAuthorModalContent extends Component {
|
||||
<Scroller className={styles.scroller}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return item.authorName.toLowerCase().includes(filter) ?
|
||||
return item.authorName.toLowerCase().includes(filterLower) ?
|
||||
(
|
||||
<SelectAuthorRow
|
||||
key={item.id}
|
||||
|
||||
@@ -52,7 +52,7 @@ class SelectBookModalContent extends Component {
|
||||
// Listeners
|
||||
|
||||
onFilterChange = ({ value }) => {
|
||||
this.setState({ filter: value.toLowerCase() });
|
||||
this.setState({ filter: value });
|
||||
};
|
||||
|
||||
//
|
||||
@@ -68,6 +68,7 @@ class SelectBookModalContent extends Component {
|
||||
} = this.props;
|
||||
|
||||
const filter = this.state.filter;
|
||||
const filterLower = filter.toLowerCase();
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -101,7 +102,7 @@ class SelectBookModalContent extends Component {
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return item.title.toLowerCase().includes(filter) ?
|
||||
return item.title.toLowerCase().includes(filterLower) ?
|
||||
(
|
||||
<SelectBookRow
|
||||
key={item.id}
|
||||
|
||||
@@ -295,11 +295,11 @@ class InteractiveImportModalContent extends Component {
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load manual import items');
|
||||
|
||||
const bulkSelectOptions = [
|
||||
{ key: SELECT, value: 'Select...', disabled: true },
|
||||
{ key: BOOK, value: 'Select Book' },
|
||||
{ key: EDITION, value: 'Select Edition' },
|
||||
{ key: QUALITY, value: 'Select Quality' },
|
||||
{ key: RELEASE_GROUP, value: 'Select ReleaseGroup' }
|
||||
{ key: SELECT, value: translate('SelectDropdown'), disabled: true },
|
||||
{ key: BOOK, value: translate('SelectBook') },
|
||||
{ key: EDITION, value: translate('SelectEdition') },
|
||||
{ key: QUALITY, value: translate('SelectQuality') },
|
||||
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') }
|
||||
];
|
||||
|
||||
if (allowAuthorChange) {
|
||||
|
||||
@@ -49,7 +49,7 @@ function EditSpecificationModalContent(props) {
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
fields && fields.some((x) => x.label === 'Regular Expression') &&
|
||||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
<div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
|
||||
|
||||
@@ -61,8 +61,12 @@ function DownloadClientOptions(props) {
|
||||
legend={translate('FailedDownloadHandling')}
|
||||
>
|
||||
<Form>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('RedownloadFailed')}</FormLabel>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
@@ -72,7 +76,28 @@ function DownloadClientOptions(props) {
|
||||
{...settings.autoRedownloadFailed}
|
||||
/>
|
||||
</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>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('RemoveDownloadsAlert')}
|
||||
</Alert>
|
||||
|
||||
@@ -14,12 +14,17 @@ function PendingChangesModal(props) {
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
bindShortcut
|
||||
bindShortcut,
|
||||
unbindShortcut
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut('enter', onConfirm);
|
||||
}, [bindShortcut, onConfirm]);
|
||||
if (isOpen) {
|
||||
bindShortcut('enter', onConfirm);
|
||||
|
||||
return () => unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -60,7 +65,8 @@ PendingChangesModal.propTypes = {
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired
|
||||
bindShortcut: PropTypes.func.isRequired,
|
||||
unbindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
PendingChangesModal.defaultProps = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||
import { fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||
import Tags from './Tags';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -25,6 +25,7 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchTags: fetchTags,
|
||||
dispatchFetchTagDetails: fetchTagDetails,
|
||||
dispatchFetchDelayProfiles: fetchDelayProfiles,
|
||||
dispatchFetchImportLists: fetchImportLists,
|
||||
@@ -41,6 +42,7 @@ class MetadatasConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
dispatchFetchTags,
|
||||
dispatchFetchTagDetails,
|
||||
dispatchFetchDelayProfiles,
|
||||
dispatchFetchImportLists,
|
||||
@@ -50,6 +52,7 @@ class MetadatasConnector extends Component {
|
||||
dispatchFetchDownloadClients
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchTags();
|
||||
dispatchFetchTagDetails();
|
||||
dispatchFetchDelayProfiles();
|
||||
dispatchFetchImportLists();
|
||||
@@ -72,6 +75,7 @@ class MetadatasConnector extends Component {
|
||||
}
|
||||
|
||||
MetadatasConnector.propTypes = {
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||
|
||||
@@ -21,19 +21,19 @@ export const firstDayOfWeekOptions = [
|
||||
];
|
||||
|
||||
export const weekColumnOptions = [
|
||||
{ key: 'ddd M/D', value: 'Tue 3/25' },
|
||||
{ key: 'ddd MM/DD', value: 'Tue 03/25' },
|
||||
{ key: 'ddd D/M', value: 'Tue 25/3' },
|
||||
{ key: 'ddd DD/MM', value: 'Tue 25/03' }
|
||||
{ key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' },
|
||||
{ key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' },
|
||||
{ key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' },
|
||||
{ key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' }
|
||||
];
|
||||
|
||||
const shortDateFormatOptions = [
|
||||
{ key: 'MMM D YYYY', value: 'Mar 25 2014' },
|
||||
{ key: 'DD MMM YYYY', value: '25 Mar 2014' },
|
||||
{ key: 'MM/D/YYYY', value: '03/25/2014' },
|
||||
{ key: 'MM/DD/YYYY', value: '03/25/2014' },
|
||||
{ key: 'DD/MM/YYYY', value: '25/03/2014' },
|
||||
{ key: 'YYYY-MM-DD', value: '2014-03-25' }
|
||||
{ key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' },
|
||||
{ key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' },
|
||||
{ key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' },
|
||||
{ key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' },
|
||||
{ key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' },
|
||||
{ key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' }
|
||||
];
|
||||
|
||||
const longDateFormatOptions = [
|
||||
|
||||
@@ -94,7 +94,12 @@ export default {
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.DESCENDING
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
|
||||
@@ -98,7 +98,12 @@ export default {
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.DESCENDING
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
@@ -146,7 +151,13 @@ export default {
|
||||
delete selectedSchema.name;
|
||||
|
||||
selectedSchema.fields = selectedSchema.fields.map((field) => {
|
||||
return { ...field };
|
||||
const newField = { ...field };
|
||||
|
||||
if (newField.privacy === 'apiKey' || newField.privacy === 'password') {
|
||||
newField.value = '';
|
||||
}
|
||||
|
||||
return newField;
|
||||
});
|
||||
|
||||
newState.selectedSchema = selectedSchema;
|
||||
|
||||
@@ -371,13 +371,14 @@ export const actionHandlers = handleThunks({
|
||||
id,
|
||||
remove,
|
||||
blocklist,
|
||||
skipRedownload
|
||||
skipRedownload,
|
||||
changeCategory
|
||||
} = payload;
|
||||
|
||||
dispatch(updateItem({ section: paged, id, isRemoving: true }));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
|
||||
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
|
||||
method: 'DELETE'
|
||||
}).request;
|
||||
|
||||
@@ -395,7 +396,8 @@ export const actionHandlers = handleThunks({
|
||||
ids,
|
||||
remove,
|
||||
blocklist,
|
||||
skipRedownload
|
||||
skipRedownload,
|
||||
changeCategory
|
||||
} = payload;
|
||||
|
||||
dispatch(batchActions([
|
||||
@@ -411,7 +413,7 @@ export const actionHandlers = handleThunks({
|
||||
]));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
|
||||
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
|
||||
method: 'DELETE',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({ ids })
|
||||
|
||||
@@ -22,9 +22,9 @@ class About extends Component {
|
||||
isNetCore,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
migrationVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
migrationVersion,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -66,13 +66,13 @@ class About extends Component {
|
||||
}
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('DBMigration')}
|
||||
data={migrationVersion}
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
title={translate('DatabaseMigration')}
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
@@ -114,9 +114,9 @@ About.propTypes = {
|
||||
isNetCore: PropTypes.bool.isRequired,
|
||||
runtimeVersion: PropTypes.string.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
databaseType: PropTypes.string.isRequired,
|
||||
databaseVersion: PropTypes.string.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
|
||||
@@ -25,20 +25,18 @@ export async function fetchTranslations(): Promise<boolean> {
|
||||
|
||||
export default function translate(
|
||||
key: string,
|
||||
tokens: Record<string, string | number | boolean> = { appName: 'Readarr' }
|
||||
tokens: Record<string, string | number | boolean> = {}
|
||||
) {
|
||||
const translation = translations[key] || key;
|
||||
|
||||
if (tokens) {
|
||||
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
||||
Object.values(tokens).forEach((value, index) => {
|
||||
tokens[index] = value;
|
||||
});
|
||||
tokens.appName = 'Readarr';
|
||||
|
||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||
String(tokens[tokenMatch] ?? match)
|
||||
);
|
||||
}
|
||||
// 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;
|
||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||
String(tokens[tokenMatch] ?? match)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,6 +124,16 @@ namespace NzbDrone.Common.Test.Http
|
||||
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]
|
||||
public async Task should_execute_https_get()
|
||||
{
|
||||
|
||||
@@ -103,31 +103,38 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
var httpClient = GetClient(request.Url);
|
||||
|
||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
try
|
||||
{
|
||||
byte[] data = null;
|
||||
|
||||
try
|
||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
{
|
||||
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)
|
||||
{
|
||||
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 (OperationCanceledException ex) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
|
||||
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]
|
||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
|
||||
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]
|
||||
@@ -101,7 +101,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook2));
|
||||
|
||||
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]
|
||||
@@ -172,7 +172,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
var decisions = new List<DownloadDecision>();
|
||||
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);
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
|
||||
|
||||
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]
|
||||
@@ -242,11 +242,11 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
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"));
|
||||
|
||||
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]
|
||||
@@ -260,12 +260,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
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"));
|
||||
|
||||
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.Torrent)), 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), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -278,7 +278,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
|
||||
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"));
|
||||
|
||||
var result = await Subject.ProcessDecisions(decisions);
|
||||
|
||||
@@ -454,6 +454,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
|
||||
|
||||
[TestCase("0")]
|
||||
[TestCase("15d")]
|
||||
[TestCase("")]
|
||||
[TestCase(null)]
|
||||
public void should_set_history_removes_completed_downloads_false(string historyRetention)
|
||||
{
|
||||
_config.Misc.history_retention = historyRetention;
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
var mock = WithUsenetClient();
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
VerifyEventPublished<BookGrabbedEvent>();
|
||||
}
|
||||
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
var mock = WithUsenetClient();
|
||||
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());
|
||||
}
|
||||
@@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
||||
.Throws(new WebException());
|
||||
|
||||
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
VerifyEventNotPublished<BookGrabbedEvent>();
|
||||
}
|
||||
@@ -121,7 +121,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
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>()
|
||||
.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));
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.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));
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.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>()))
|
||||
.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>()
|
||||
.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());
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
||||
@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
[Test]
|
||||
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());
|
||||
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());
|
||||
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 mockUsenet = WithUsenetClient();
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
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());
|
||||
@@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
|
||||
_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());
|
||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
||||
|
||||
results.Should().NotBeEmpty();
|
||||
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]
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.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();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
|
||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
{
|
||||
[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-03-15 00:00:00Z")]
|
||||
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
||||
{
|
||||
private MetadataProfile _metadataProfile;
|
||||
|
||||
@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
|
||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
{
|
||||
[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-03-15 00:00:00Z")]
|
||||
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
||||
{
|
||||
[SetUp]
|
||||
|
||||
@@ -166,6 +166,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||
|
||||
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
|
||||
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
|
||||
.Excluding(x => x.AuthorId);
|
||||
.Excluding(x => x.AuthorId)
|
||||
.Excluding(x => x.ForeignEditionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Blocklisting
|
||||
public interface IBlocklistService
|
||||
{
|
||||
bool Blocklisted(int authorId, ReleaseInfo release);
|
||||
bool BlocklistedTorrentHash(int authorId, string hash);
|
||||
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
||||
void Block(RemoteBook remoteEpisode, string message);
|
||||
void Delete(int id);
|
||||
@@ -36,30 +37,34 @@ namespace NzbDrone.Core.Blocklisting
|
||||
|
||||
public bool Blocklisted(int authorId, ReleaseInfo release)
|
||||
{
|
||||
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(authorId, release.Title);
|
||||
|
||||
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
var torrentInfo = release as TorrentInfo;
|
||||
|
||||
if (torrentInfo == null)
|
||||
if (release is not TorrentInfo torrentInfo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (torrentInfo.InfoHash.IsNullOrWhiteSpace())
|
||||
if (torrentInfo.InfoHash.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
||||
.Any(b => SameTorrent(b, torrentInfo));
|
||||
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, torrentInfo.InfoHash);
|
||||
|
||||
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
||||
}
|
||||
|
||||
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, torrentInfo.InfoHash);
|
||||
|
||||
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
||||
return _blocklistRepository.BlocklistedByTitle(authorId, release.Title)
|
||||
.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
||||
.Any(b => SameTorrent(b, torrentInfo));
|
||||
}
|
||||
|
||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet)
|
||||
.Any(b => SameNzb(b, release));
|
||||
return _blocklistRepository.BlocklistedByTitle(authorId, release.Title)
|
||||
.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)
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace NzbDrone.Core.Books
|
||||
// These are metadata entries
|
||||
public int AuthorMetadataId { get; set; }
|
||||
public string ForeignBookId { get; set; }
|
||||
public string ForeignEditionId { get; set; }
|
||||
public string TitleSlug { get; set; }
|
||||
public string Title { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
@@ -33,6 +34,7 @@ namespace NzbDrone.Core.Books
|
||||
public List<string> Genres { get; set; }
|
||||
public List<int> RelatedBooks { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
public DateTime? LastSearchTime { get; set; }
|
||||
|
||||
// These are Readarr generated/config
|
||||
public string CleanTitle { get; set; }
|
||||
@@ -71,6 +73,7 @@ namespace NzbDrone.Core.Books
|
||||
public override void UseMetadataFrom(Book other)
|
||||
{
|
||||
ForeignBookId = other.ForeignBookId;
|
||||
ForeignEditionId = other.ForeignEditionId;
|
||||
TitleSlug = other.TitleSlug;
|
||||
Title = other.Title;
|
||||
ReleaseDate = other.ReleaseDate;
|
||||
@@ -88,6 +91,7 @@ namespace NzbDrone.Core.Books
|
||||
Monitored = other.Monitored;
|
||||
AnyEditionOk = other.AnyEditionOk;
|
||||
LastInfoSync = other.LastInfoSync;
|
||||
LastSearchTime = other.LastSearchTime;
|
||||
Added = other.Added;
|
||||
AddOptions = other.AddOptions;
|
||||
}
|
||||
@@ -95,6 +99,7 @@ namespace NzbDrone.Core.Books
|
||||
public override void ApplyChanges(Book other)
|
||||
{
|
||||
ForeignBookId = other.ForeignBookId;
|
||||
ForeignEditionId = other.ForeignEditionId;
|
||||
AddOptions = other.AddOptions;
|
||||
Monitored = other.Monitored;
|
||||
AnyEditionOk = other.AnyEditionOk;
|
||||
|
||||
@@ -225,7 +225,12 @@ namespace NzbDrone.Core.Books
|
||||
public Author UpdateAuthor(Author author)
|
||||
{
|
||||
_cache.Clear();
|
||||
|
||||
var storedAuthor = GetAuthor(author.Id);
|
||||
|
||||
// Never update AddOptions when updating an author, keep it the same as the existing stored author.
|
||||
author.AddOptions = storedAuthor.AddOptions;
|
||||
|
||||
var updatedAuthor = _authorRepository.Update(author);
|
||||
_eventAggregator.PublishEvent(new AuthorEditedEvent(updatedAuthor, storedAuthor));
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace NzbDrone.Core.Books
|
||||
Book UpdateBook(Book book);
|
||||
void SetBookMonitored(int bookId, bool monitored);
|
||||
void SetMonitored(IEnumerable<int> ids, bool monitored);
|
||||
void UpdateLastSearchTime(List<Book> books);
|
||||
PagingSpec<Book> BooksWithoutFiles(PagingSpec<Book> pagingSpec);
|
||||
List<Book> BooksBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
|
||||
List<Book> AuthorBooksBetweenDates(Author author, DateTime start, DateTime end, bool includeUnmonitored);
|
||||
@@ -303,6 +304,11 @@ namespace NzbDrone.Core.Books
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateLastSearchTime(List<Book> books)
|
||||
{
|
||||
_bookRepository.SetFields(books, b => b.LastSearchTime);
|
||||
}
|
||||
|
||||
public void Handle(AuthorDeletedEvent message)
|
||||
{
|
||||
var books = GetBooksByAuthorMetadataId(message.Author.AuthorMetadataId);
|
||||
|
||||
@@ -144,6 +144,13 @@ namespace NzbDrone.Core.Configuration
|
||||
set { SetValue("AutoRedownloadFailed", value); }
|
||||
}
|
||||
|
||||
public bool AutoRedownloadFailedFromInteractiveSearch
|
||||
{
|
||||
get { return GetValueBoolean("AutoRedownloadFailedFromInteractiveSearch", true); }
|
||||
|
||||
set { SetValue("AutoRedownloadFailedFromInteractiveSearch", value); }
|
||||
}
|
||||
|
||||
public bool CreateEmptyAuthorFolders
|
||||
{
|
||||
get { return GetValueBoolean("CreateEmptyAuthorFolders", false); }
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Configuration
|
||||
//Completed/Failed Download Handling (Download client)
|
||||
bool EnableCompletedDownloadHandling { get; set; }
|
||||
bool AutoRedownloadFailed { get; set; }
|
||||
bool AutoRedownloadFailedFromInteractiveSearch { get; set; }
|
||||
|
||||
//Media Management
|
||||
bool AutoUnmonitorPreviouslyDownloadedBooks { get; set; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Books;
|
||||
@@ -23,10 +24,12 @@ namespace NzbDrone.Core.CustomFormats
|
||||
public class CustomFormatCalculationService : ICustomFormatCalculationService
|
||||
{
|
||||
private readonly ICustomFormatService _formatService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public CustomFormatCalculationService(ICustomFormatService formatService)
|
||||
public CustomFormatCalculationService(ICustomFormatService formatService, Logger logger)
|
||||
{
|
||||
_formatService = formatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<CustomFormat> ParseCustomFormat(RemoteBook remoteBook, long size)
|
||||
@@ -145,26 +148,30 @@ namespace NzbDrone.Core.CustomFormats
|
||||
return matches.OrderBy(x => x.Name).ToList();
|
||||
}
|
||||
|
||||
private static List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author, List<CustomFormat> allCustomFormats)
|
||||
private List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author, List<CustomFormat> allCustomFormats)
|
||||
{
|
||||
var sceneName = string.Empty;
|
||||
var releaseTitle = string.Empty;
|
||||
|
||||
if (bookFile.SceneName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
sceneName = bookFile.SceneName;
|
||||
_logger.Trace("Using scene name for release title: {0}", bookFile.SceneName);
|
||||
releaseTitle = bookFile.SceneName;
|
||||
}
|
||||
else if (bookFile.OriginalFilePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
sceneName = bookFile.OriginalFilePath;
|
||||
_logger.Trace("Using original file path for release title: {0}", bookFile.OriginalFilePath);
|
||||
releaseTitle = bookFile.OriginalFilePath;
|
||||
}
|
||||
else if (bookFile.Path.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
sceneName = Path.GetFileName(bookFile.Path);
|
||||
_logger.Trace("Using path for release title: {0}", Path.GetFileName(bookFile.Path));
|
||||
releaseTitle = Path.GetFileName(bookFile.Path);
|
||||
}
|
||||
|
||||
var bookInfo = new ParsedBookInfo
|
||||
{
|
||||
AuthorName = author.Name,
|
||||
ReleaseTitle = sceneName,
|
||||
ReleaseTitle = releaseTitle,
|
||||
Quality = bookFile.Quality,
|
||||
ReleaseGroup = bookFile.ReleaseGroup
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.CustomFormats
|
||||
protected Regex _regex;
|
||||
protected string _raw;
|
||||
|
||||
[FieldDefinition(1, Label = "Regular Expression", HelpText = "Custom Format RegEx is Case Insensitive")]
|
||||
[FieldDefinition(1, Label = "CustomFormatsSpecificationRegularExpression", HelpText = "CustomFormatsSpecificationRegularExpressionHelpText")]
|
||||
public string Value
|
||||
{
|
||||
get => _raw;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.SQLite;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
@@ -40,14 +41,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
if (db.ConnectionString.Contains(".db"))
|
||||
{
|
||||
return DatabaseType.SQLite;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DatabaseType.PostgreSQL;
|
||||
}
|
||||
return db is SQLiteConnection ? DatabaseType.SQLite : DatabaseType.PostgreSQL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentMigrator;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
|
||||
public class DatabaseEngineVersionCheck : FluentMigrator.Migration
|
||||
{
|
||||
protected readonly Logger _logger;
|
||||
|
||||
public DatabaseEngineVersionCheck()
|
||||
{
|
||||
_logger = NzbDroneLogger.GetLogger(this);
|
||||
}
|
||||
|
||||
public override void Up()
|
||||
{
|
||||
IfDatabase("sqlite").Execute.WithConnection(LogSqliteVersion);
|
||||
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
|
||||
}
|
||||
|
||||
public override void Down()
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using (var versionCmd = conn.CreateCommand())
|
||||
{
|
||||
versionCmd.Transaction = tran;
|
||||
versionCmd.CommandText = "SELECT sqlite_version();";
|
||||
|
||||
using (var reader = versionCmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var version = reader.GetString(0);
|
||||
|
||||
_logger.Info("SQLite {0}", version);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LogPostgresVersion(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using (var versionCmd = conn.CreateCommand())
|
||||
{
|
||||
versionCmd.Transaction = tran;
|
||||
versionCmd.CommandText = "SHOW server_version";
|
||||
|
||||
using (var reader = versionCmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var version = reader.GetString(0);
|
||||
var cleanVersion = Regex.Replace(version, @"\(.*?\)", "");
|
||||
|
||||
_logger.Info("Postgres {0}", cleanVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Delete.FromTable("Commands").AllRows();
|
||||
|
||||
Alter.Table("Authors").AlterColumn("LastInfoSync").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("Authors").AlterColumn("Added").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("AuthorMetadata").AlterColumn("Born").AsDateTimeOffset().Nullable();
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(039)]
|
||||
public class book_last_searched_time : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Books").AddColumn("LastSearchTime").AsDateTimeOffset().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,12 +42,13 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
serviceProvider = new ServiceCollection()
|
||||
.AddLogging(b => b.AddNLog())
|
||||
.AddFluentMigratorCore()
|
||||
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
|
||||
.ConfigureRunner(
|
||||
builder => builder
|
||||
.AddPostgres()
|
||||
.AddNzbDroneSQLite()
|
||||
.WithGlobalConnectionString(connectionString)
|
||||
.WithMigrationsIn(Assembly.GetExecutingAssembly()))
|
||||
.ScanIn(Assembly.GetExecutingAssembly()).For.All())
|
||||
.Configure<TypeFilterOptions>(opt => opt.Namespace = "NzbDrone.Core.Datastore.Migration")
|
||||
.Configure<ProcessorOptions>(opt =>
|
||||
{
|
||||
|
||||
@@ -135,6 +135,7 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
Mapper.Entity<Book>("Books").RegisterModel()
|
||||
.Ignore(x => x.AuthorId)
|
||||
.Ignore(x => x.ForeignEditionId)
|
||||
.HasOne(r => r.AuthorMetadata, r => r.AuthorMetadataId)
|
||||
.LazyLoad(x => x.BookFiles,
|
||||
(db, book) => db.Query<BookFile>(new SqlBuilder(db.DatabaseType)
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.DecisionEngine
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -42,17 +42,17 @@ namespace NzbDrone.Core.DecisionEngine
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports)
|
||||
public List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports, bool pushedRelease = false)
|
||||
{
|
||||
return GetBookDecisions(reports).ToList();
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
@@ -206,6 +206,26 @@ namespace NzbDrone.Core.DecisionEngine
|
||||
|
||||
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())
|
||||
{
|
||||
_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.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -27,8 +28,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
}
|
||||
@@ -130,7 +132,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
||||
CanMoveFiles = false,
|
||||
CanBeRemoved = torrent.Status == "complete",
|
||||
Category = null,
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = torrent.InfoHash?.ToUpper(),
|
||||
IsEncrypted = false,
|
||||
Message = torrent.ErrorMessage,
|
||||
|
||||
@@ -7,6 +7,7 @@ using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Organizer;
|
||||
@@ -29,8 +30,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_scanWatchFolder = scanWatchFolder;
|
||||
|
||||
@@ -87,7 +89,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||
{
|
||||
yield return new DownloadClientItem
|
||||
{
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = Definition.Name + "_" + item.DownloadId,
|
||||
Category = "Readarr",
|
||||
Title = item.Title,
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||
{
|
||||
yield return new DownloadClientItem
|
||||
{
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = Definition.Name + "_" + item.DownloadId,
|
||||
Category = "Readarr",
|
||||
Title = item.Title,
|
||||
|
||||
@@ -7,6 +7,7 @@ using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -25,8 +26,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
}
|
||||
@@ -133,7 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
item.Title = torrent.Name;
|
||||
item.Category = Settings.MusicCategory;
|
||||
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.MusicImportedCategory.IsNotNullOrWhiteSpace());
|
||||
|
||||
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath));
|
||||
item.OutputPath = outputPath + torrent.Name;
|
||||
|
||||
@@ -8,6 +8,7 @@ using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
@@ -36,8 +37,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_dsInfoProxy = dsInfoProxy;
|
||||
_dsTaskProxySelector = dsTaskProxySelector;
|
||||
@@ -87,7 +89,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
var item = new DownloadClientItem()
|
||||
{
|
||||
Category = Settings.MusicCategory,
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = CreateDownloadId(torrent.Id, serialNumber),
|
||||
Title = torrent.Title,
|
||||
TotalSize = torrent.Size,
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
var item = new DownloadClientItem()
|
||||
{
|
||||
Category = Settings.MusicCategory,
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = CreateDownloadId(nzb.Id, serialNumber),
|
||||
Title = nzb.Title,
|
||||
TotalSize = nzb.Size,
|
||||
|
||||
@@ -6,6 +6,7 @@ using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download.Clients.Flood.Models;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
@@ -27,8 +28,9 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_downloadSeedConfigProvider = downloadSeedConfigProvider;
|
||||
@@ -107,7 +109,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
||||
|
||||
var item = new DownloadClientItem
|
||||
{
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = torrent.Key,
|
||||
Title = properties.Name,
|
||||
OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(properties.Directory)),
|
||||
|
||||
@@ -5,6 +5,7 @@ using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download.Clients.Hadouken.Models;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
@@ -24,8 +25,9 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
}
|
||||
@@ -55,7 +57,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
||||
|
||||
var item = new DownloadClientItem
|
||||
{
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = torrent.InfoHash.ToUpper(),
|
||||
OutputPath = outputPath + torrent.Name,
|
||||
RemainingSize = torrent.TotalSize - torrent.DownloadedBytes,
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
{
|
||||
var queueItem = new DownloadClientItem();
|
||||
|
||||
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
|
||||
queueItem.DownloadId = vortexQueueItem.AddUUID ?? vortexQueueItem.Id.ToString();
|
||||
queueItem.Category = vortexQueueItem.GroupName;
|
||||
queueItem.Title = vortexQueueItem.UiTitle;
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
queueItem.Title = item.NzbName;
|
||||
queueItem.TotalSize = totalSize;
|
||||
queueItem.Category = item.Category;
|
||||
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
|
||||
queueItem.CanMoveFiles = true;
|
||||
queueItem.CanBeRemoved = true;
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
var historyItem = new DownloadClientItem();
|
||||
var itemDir = item.FinalDir.IsNullOrWhiteSpace() ? item.DestDir : item.FinalDir;
|
||||
|
||||
historyItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
historyItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
|
||||
historyItem.DownloadId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString();
|
||||
historyItem.Title = item.Name;
|
||||
historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo);
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
|
||||
|
||||
var historyItem = new DownloadClientItem
|
||||
{
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = GetDownloadClientId(file),
|
||||
Title = title,
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -34,8 +35,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
ICacheManager cacheManager,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_proxySelector = proxySelector;
|
||||
|
||||
@@ -229,7 +231,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
|
||||
Title = torrent.Name,
|
||||
TotalSize = torrent.Size,
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.MusicImportedCategory.IsNotNullOrWhiteSpace()),
|
||||
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
|
||||
RemainingTime = GetRemainingTime(torrent),
|
||||
SeedRatio = torrent.Ratio
|
||||
@@ -611,7 +613,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
|
||||
{
|
||||
if (torrent.Ratio >= config.MaxRatio)
|
||||
if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
}
|
||||
|
||||
var queueItem = new DownloadClientItem();
|
||||
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
|
||||
queueItem.DownloadId = sabQueueItem.Id;
|
||||
queueItem.Category = sabQueueItem.Category;
|
||||
queueItem.Title = sabQueueItem.Title;
|
||||
@@ -118,7 +118,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
|
||||
var historyItem = new DownloadClientItem
|
||||
{
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = sabHistoryItem.Id,
|
||||
Category = sabHistoryItem.Category,
|
||||
Title = sabHistoryItem.Title,
|
||||
@@ -263,7 +263,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
|
||||
}
|
||||
|
||||
if (config.Misc.history_retention.IsNotNullOrWhiteSpace() && config.Misc.history_retention.EndsWith("d"))
|
||||
if (config.Misc.history_retention.IsNullOrWhiteSpace())
|
||||
{
|
||||
status.RemovesCompletedDownloads = false;
|
||||
}
|
||||
else if (config.Misc.history_retention.EndsWith("d"))
|
||||
{
|
||||
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
|
||||
out var daysRetention);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.RemotePathMappings;
|
||||
@@ -18,8 +19,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
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.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -24,8 +25,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
}
|
||||
@@ -70,7 +72,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
item.Category = Settings.MusicCategory;
|
||||
item.Title = torrent.Name;
|
||||
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
|
||||
|
||||
item.OutputPath = GetOutputPath(outputPath, torrent);
|
||||
item.TotalSize = torrent.TotalSize;
|
||||
|
||||
@@ -2,6 +2,7 @@ using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download.Clients.Transmission;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
@@ -19,8 +20,9 @@ namespace NzbDrone.Core.Download.Clients.Vuze
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
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.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download.Clients.rTorrent;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
@@ -34,8 +35,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IDownloadSeedConfigProvider downloadSeedConfigProvider,
|
||||
IRTorrentDirectoryValidator rTorrentDirectoryValidator,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_rTorrentDirectoryValidator = rTorrentDirectoryValidator;
|
||||
@@ -144,7 +146,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
}
|
||||
|
||||
var item = new DownloadClientItem();
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.MusicImportedCategory.IsNotNullOrWhiteSpace());
|
||||
item.Title = torrent.Name;
|
||||
item.DownloadId = torrent.Hash;
|
||||
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path));
|
||||
|
||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -28,8 +29,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
|
||||
@@ -118,7 +120,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
item.Title = torrent.Name;
|
||||
item.TotalSize = torrent.Size;
|
||||
item.Category = torrent.Label;
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.MusicImportedCategory.IsNotNullOrWhiteSpace());
|
||||
item.RemainingSize = torrent.Remaining;
|
||||
item.SeedRatio = torrent.Ratio;
|
||||
|
||||
|
||||
@@ -38,8 +38,10 @@ namespace NzbDrone.Core.Download
|
||||
public string Type { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool HasPostImportCategory { get; set; }
|
||||
|
||||
public static DownloadClientItemClientInfo FromDownloadClient<TSettings>(DownloadClientBase<TSettings> downloadClient)
|
||||
public static DownloadClientItemClientInfo FromDownloadClient<TSettings>(
|
||||
DownloadClientBase<TSettings> downloadClient, bool hasPostImportCategory)
|
||||
where TSettings : IProviderConfig, new()
|
||||
{
|
||||
return new DownloadClientItemClientInfo
|
||||
@@ -47,7 +49,8 @@ namespace NzbDrone.Core.Download
|
||||
Protocol = downloadClient.Protocol,
|
||||
Type = downloadClient.Name,
|
||||
Id = downloadClient.Definition.Id,
|
||||
Name = downloadClient.Definition.Name
|
||||
Name = downloadClient.Definition.Name,
|
||||
HasPostImportCategory = hasPostImportCategory
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,13 +59,18 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
var indexer = _indexerFactory.Find(indexerId);
|
||||
|
||||
if (indexer != null && indexer.DownloadClientId > 0)
|
||||
if (indexer is { DownloadClientId: > 0 })
|
||||
{
|
||||
var client = availableProviders.SingleOrDefault(d => d.Definition.Id == indexer.DownloadClientId);
|
||||
|
||||
if (client == null || (filterBlockedClients && blockedProviders.Contains(client.Definition.Id)))
|
||||
if (client == null)
|
||||
{
|
||||
throw new DownloadClientUnavailableException($"Indexer specified download client is not available");
|
||||
throw new DownloadClientUnavailableException($"Indexer specified download client does not exist for {indexer.Name}");
|
||||
}
|
||||
|
||||
if (filterBlockedClients && blockedProviders.Contains(client.Definition.Id))
|
||||
{
|
||||
throw new DownloadClientUnavailableException($"Indexer specified download client is not available due to recent failures for {indexer.Name}");
|
||||
}
|
||||
|
||||
return client;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
@@ -22,5 +23,6 @@ namespace NzbDrone.Core.Download
|
||||
public Dictionary<string, string> Data { get; set; }
|
||||
public TrackedDownload TrackedDownload { get; set; }
|
||||
public bool SkipRedownload { get; set; }
|
||||
public ReleaseSourceType ReleaseSource { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
public interface IDownloadService
|
||||
{
|
||||
Task DownloadReport(RemoteBook remoteBook);
|
||||
Task DownloadReport(RemoteBook remoteBook, int? downloadClientId);
|
||||
}
|
||||
|
||||
public class DownloadService : IDownloadService
|
||||
@@ -50,13 +50,15 @@ namespace NzbDrone.Core.Download
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task DownloadReport(RemoteBook remoteBook)
|
||||
public async Task DownloadReport(RemoteBook remoteBook, int? downloadClientId)
|
||||
{
|
||||
var filterBlockedClients = remoteBook.Release.PendingReleaseReason == PendingReleaseReason.DownloadClientUnavailable;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -102,6 +104,11 @@ namespace NzbDrone.Core.Download
|
||||
_logger.Trace("Release {0} no longer available on indexer.", remoteBook);
|
||||
throw;
|
||||
}
|
||||
catch (ReleaseBlockedException)
|
||||
{
|
||||
_logger.Trace("Release {0} previously added to blocklist, not sending to download client again.", remoteBook);
|
||||
throw;
|
||||
}
|
||||
catch (DownloadClientRejectedReleaseException)
|
||||
{
|
||||
_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.DownloadClientName = downloadClient.Definition.Name;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(downloadClientId))
|
||||
if (downloadClientId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
bookGrabbedEvent.DownloadId = downloadClientId;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -129,7 +132,8 @@ namespace NzbDrone.Core.Download
|
||||
Message = message,
|
||||
Data = historyItem.Data,
|
||||
TrackedDownload = trackedDownload,
|
||||
SkipRedownload = skipRedownload
|
||||
SkipRedownload = skipRedownload,
|
||||
ReleaseSource = releaseSource
|
||||
};
|
||||
|
||||
_eventAggregator.PublishEvent(downloadFailedEvent);
|
||||
|
||||
@@ -12,8 +12,14 @@ namespace NzbDrone.Core.Download.Pending
|
||||
public ParsedBookInfo ParsedBookInfo { get; set; }
|
||||
public ReleaseInfo Release { get; set; }
|
||||
public PendingReleaseReason Reason { get; set; }
|
||||
public PendingReleaseAdditionalInfo AdditionalInfo { get; set; }
|
||||
|
||||
//Not persisted
|
||||
public RemoteBook RemoteBook { get; set; }
|
||||
}
|
||||
|
||||
public class PendingReleaseAdditionalInfo
|
||||
{
|
||||
public ReleaseSourceType ReleaseSource { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ namespace NzbDrone.Core.Download.Pending
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly IRemoteBookAggregationService _aggregationService;
|
||||
private readonly IDownloadClientFactory _downloadClientFactory;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -58,6 +60,8 @@ namespace NzbDrone.Core.Download.Pending
|
||||
IConfigService configService,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IRemoteBookAggregationService aggregationService,
|
||||
IDownloadClientFactory downloadClientFactory,
|
||||
IIndexerFactory indexerFactory,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
@@ -70,6 +74,8 @@ namespace NzbDrone.Core.Download.Pending
|
||||
_configService = configService;
|
||||
_formatCalculator = formatCalculator;
|
||||
_aggregationService = aggregationService;
|
||||
_downloadClientFactory = downloadClientFactory;
|
||||
_indexerFactory = indexerFactory;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -107,9 +113,16 @@ namespace NzbDrone.Core.Download.Pending
|
||||
|
||||
if (matchingReport.Reason != reason)
|
||||
{
|
||||
_logger.Debug("The release {0} is already pending with reason {1}, changing to {2}", decision.RemoteBook, matchingReport.Reason, reason);
|
||||
matchingReport.Reason = reason;
|
||||
_repository.Update(matchingReport);
|
||||
if (matchingReport.Reason == PendingReleaseReason.DownloadClientUnavailable)
|
||||
{
|
||||
_logger.Debug("The release {0} is already pending with reason {1}, not changing reason", decision.RemoteBook, matchingReport.Reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("The release {0} is already pending with reason {1}, changing to {2}", decision.RemoteBook, matchingReport.Reason, reason);
|
||||
matchingReport.Reason = reason;
|
||||
_repository.Update(matchingReport);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -190,6 +203,16 @@ namespace NzbDrone.Core.Download.Pending
|
||||
timeleft = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
string downloadClientName = null;
|
||||
var indexer = _indexerFactory.Find(pendingRelease.Release.IndexerId);
|
||||
|
||||
if (indexer is { DownloadClientId: > 0 })
|
||||
{
|
||||
var downloadClient = _downloadClientFactory.Find(indexer.DownloadClientId);
|
||||
|
||||
downloadClientName = downloadClient?.Name;
|
||||
}
|
||||
|
||||
var queue = new Queue.Queue
|
||||
{
|
||||
Id = GetQueueId(pendingRelease, book),
|
||||
@@ -204,7 +227,8 @@ namespace NzbDrone.Core.Download.Pending
|
||||
EstimatedCompletionTime = ect,
|
||||
Status = pendingRelease.Reason.ToString(),
|
||||
Protocol = pendingRelease.RemoteBook.Release.DownloadProtocol,
|
||||
Indexer = pendingRelease.RemoteBook.Release.Indexer
|
||||
Indexer = pendingRelease.RemoteBook.Release.Indexer,
|
||||
DownloadClient = downloadClientName
|
||||
};
|
||||
|
||||
queued.Add(queue);
|
||||
@@ -320,6 +344,7 @@ namespace NzbDrone.Core.Download.Pending
|
||||
{
|
||||
Author = author,
|
||||
Books = books,
|
||||
ReleaseSource = release.AdditionalInfo?.ReleaseSource ?? ReleaseSourceType.Unknown,
|
||||
ParsedBookInfo = release.ParsedBookInfo,
|
||||
Release = release.Release
|
||||
};
|
||||
@@ -342,7 +367,11 @@ namespace NzbDrone.Core.Download.Pending
|
||||
Release = decision.RemoteBook.Release,
|
||||
Title = decision.RemoteBook.Release.Title,
|
||||
Added = DateTime.UtcNow,
|
||||
Reason = reason
|
||||
Reason = reason,
|
||||
AdditionalInfo = new PendingReleaseAdditionalInfo
|
||||
{
|
||||
ReleaseSource = decision.RemoteBook.ReleaseSource
|
||||
}
|
||||
});
|
||||
|
||||
_eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent());
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace NzbDrone.Core.Download
|
||||
public interface IProcessDownloadDecisions
|
||||
{
|
||||
Task<ProcessedDecisions> ProcessDecisions(List<DownloadDecision> decisions);
|
||||
Task<ProcessedDecisionResult> ProcessDecision(DownloadDecision decision, int? downloadClientId);
|
||||
}
|
||||
|
||||
public class ProcessDownloadDecisions : IProcessDownloadDecisions
|
||||
@@ -40,8 +41,6 @@ namespace NzbDrone.Core.Download
|
||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports);
|
||||
var grabbed = new List<DownloadDecision>();
|
||||
var pending = new List<DownloadDecision>();
|
||||
|
||||
//var failed = new List<DownloadDecision>();
|
||||
var rejected = decisions.Where(d => d.Rejected).ToList();
|
||||
|
||||
var pendingAddQueue = new List<Tuple<DownloadDecision, PendingReleaseReason>>();
|
||||
@@ -51,7 +50,6 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
foreach (var report in prioritizedDecisions)
|
||||
{
|
||||
var remoteBook = report.RemoteBook;
|
||||
var downloadProtocol = report.RemoteBook.Release.DownloadProtocol;
|
||||
|
||||
//Skip if already grabbed
|
||||
@@ -73,37 +71,48 @@ namespace NzbDrone.Core.Download
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_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);
|
||||
var result = await ProcessDecisionInternal(report);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
//Process both approved and temporarily rejected
|
||||
return decisions.Where(c => (c.Approved || c.TemporarilyRejected) && c.RemoteBook.Books.Any()).ToList();
|
||||
return decisions.Where(IsQualifiedReport).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)
|
||||
@@ -148,5 +191,38 @@ namespace NzbDrone.Core.Download
|
||||
queue.Add(Tuple.Create(report, reason));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
src/NzbDrone.Core/Download/ProcessedDecisionResult.cs
Normal file
11
src/NzbDrone.Core/Download/ProcessedDecisionResult.cs
Normal file
@@ -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.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
@@ -41,6 +42,12 @@ namespace NzbDrone.Core.Download
|
||||
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)
|
||||
{
|
||||
_logger.Debug("Failed download only contains one book, searching again");
|
||||
|
||||
@@ -6,6 +6,7 @@ using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
@@ -21,17 +22,20 @@ namespace NzbDrone.Core.Download
|
||||
where TSettings : IProviderConfig, new()
|
||||
{
|
||||
protected readonly IHttpClient _httpClient;
|
||||
private readonly IBlocklistService _blocklistService;
|
||||
protected readonly ITorrentFileInfoReader _torrentFileInfoReader;
|
||||
|
||||
protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader,
|
||||
IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
Logger logger)
|
||||
IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(configService, diskProvider, remotePathMappingService, logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_blocklistService = blocklistService;
|
||||
_torrentFileInfoReader = torrentFileInfoReader;
|
||||
}
|
||||
|
||||
@@ -86,7 +90,7 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
try
|
||||
{
|
||||
return DownloadFromMagnetUrl(remoteBook, magnetUrl);
|
||||
return DownloadFromMagnetUrl(remoteBook, indexer, magnetUrl);
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
@@ -100,7 +104,7 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
try
|
||||
{
|
||||
return DownloadFromMagnetUrl(remoteBook, magnetUrl);
|
||||
return DownloadFromMagnetUrl(remoteBook, indexer, magnetUrl);
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
@@ -149,7 +153,7 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
if (locationHeader.StartsWith("magnet:"))
|
||||
{
|
||||
return DownloadFromMagnetUrl(remoteBook, locationHeader);
|
||||
return DownloadFromMagnetUrl(remoteBook, indexer, locationHeader);
|
||||
}
|
||||
|
||||
request.Url += new HttpUri(locationHeader);
|
||||
@@ -192,6 +196,9 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteBook.Release.Title));
|
||||
var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile);
|
||||
|
||||
EnsureReleaseIsNotBlocklisted(remoteBook, indexer, hash);
|
||||
|
||||
var actualHash = AddFromTorrentFile(remoteBook, hash, filename, torrentFile);
|
||||
|
||||
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
|
||||
@@ -205,7 +212,7 @@ namespace NzbDrone.Core.Download
|
||||
return actualHash;
|
||||
}
|
||||
|
||||
private string DownloadFromMagnetUrl(RemoteBook remoteBook, string magnetUrl)
|
||||
private string DownloadFromMagnetUrl(RemoteBook remoteBook, IIndexer indexer, string magnetUrl)
|
||||
{
|
||||
string hash = null;
|
||||
string actualHash = null;
|
||||
@@ -223,6 +230,8 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
if (hash != null)
|
||||
{
|
||||
EnsureReleaseIsNotBlocklisted(remoteBook, indexer, hash);
|
||||
|
||||
actualHash = AddFromMagnetLink(remoteBook, hash, magnetUrl);
|
||||
}
|
||||
|
||||
@@ -236,5 +245,29 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
src/NzbDrone.Core/Exceptions/ReleaseBlockedException.cs
Normal file
28
src/NzbDrone.Core/Exceptions/ReleaseBlockedException.cs
Normal file
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Extras.Files
|
||||
List<TExtraFile> GetFilesByAuthor(int authorId);
|
||||
List<TExtraFile> GetFilesByBook(int authorId, int bookId);
|
||||
List<TExtraFile> GetFilesByBookFile(int bookFileId);
|
||||
TExtraFile FindByPath(string path);
|
||||
TExtraFile FindByPath(int authorId, string path);
|
||||
}
|
||||
|
||||
public class ExtraFileRepository<TExtraFile> : BasicRepository<TExtraFile>, IExtraFileRepository<TExtraFile>
|
||||
@@ -55,9 +55,9 @@ namespace NzbDrone.Core.Extras.Files
|
||||
return Query(c => c.BookFileId == bookFileId);
|
||||
}
|
||||
|
||||
public TExtraFile FindByPath(string path)
|
||||
public TExtraFile FindByPath(int authorId, string path)
|
||||
{
|
||||
return Query(c => c.RelativePath == path).SingleOrDefault();
|
||||
return Query(c => c.AuthorId == authorId && c.RelativePath == path).SingleOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace NzbDrone.Core.Extras.Files
|
||||
{
|
||||
List<TExtraFile> GetFilesByAuthor(int authorId);
|
||||
List<TExtraFile> GetFilesByBookFile(int bookFileId);
|
||||
TExtraFile FindByPath(string path);
|
||||
TExtraFile FindByPath(int authorId, string path);
|
||||
void Upsert(TExtraFile extraFile);
|
||||
void Upsert(List<TExtraFile> extraFiles);
|
||||
void Delete(int id);
|
||||
@@ -59,9 +59,9 @@ namespace NzbDrone.Core.Extras.Files
|
||||
return _repository.GetFilesByBookFile(bookFileId);
|
||||
}
|
||||
|
||||
public TExtraFile FindByPath(string path)
|
||||
public TExtraFile FindByPath(int authorId, string path)
|
||||
{
|
||||
return _repository.FindByPath(path);
|
||||
return _repository.FindByPath(authorId, path);
|
||||
}
|
||||
|
||||
public void Upsert(TExtraFile extraFile)
|
||||
|
||||
@@ -41,8 +41,8 @@ namespace NzbDrone.Core.Extras.Others
|
||||
}
|
||||
|
||||
var relativePath = author.Path.GetRelativePath(path);
|
||||
var otherExtraFile = _otherExtraFileService.FindByPath(author.Id, relativePath);
|
||||
|
||||
var otherExtraFile = _otherExtraFileService.FindByPath(relativePath);
|
||||
if (otherExtraFile != null)
|
||||
{
|
||||
var newPath = path + "-orig";
|
||||
@@ -66,8 +66,8 @@ namespace NzbDrone.Core.Extras.Others
|
||||
}
|
||||
|
||||
var relativePath = author.Path.GetRelativePath(path);
|
||||
var otherExtraFile = _otherExtraFileService.FindByPath(author.Id, relativePath);
|
||||
|
||||
var otherExtraFile = _otherExtraFileService.FindByPath(relativePath);
|
||||
if (otherExtraFile != null)
|
||||
{
|
||||
var subfolder = Path.GetDirectoryName(relativePath);
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.History
|
||||
public class EntityHistory : ModelBase
|
||||
{
|
||||
public const string DOWNLOAD_CLIENT = "downloadClient";
|
||||
public const string RELEASE_SOURCE = "releaseSource";
|
||||
|
||||
public EntityHistory()
|
||||
{
|
||||
|
||||
@@ -162,15 +162,15 @@ namespace NzbDrone.Core.History
|
||||
history.Data.Add("Guid", message.Book.Release.Guid);
|
||||
history.Data.Add("Protocol", ((int)message.Book.Release.DownloadProtocol).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())
|
||||
{
|
||||
history.Data.Add("ReleaseHash", message.Book.ParsedBookInfo.ReleaseHash);
|
||||
}
|
||||
|
||||
var torrentRelease = message.Book.Release as TorrentInfo;
|
||||
|
||||
if (torrentRelease != null)
|
||||
if (message.Book.Release is TorrentInfo torrentRelease)
|
||||
{
|
||||
history.Data.Add("TorrentInfoHash", torrentRelease.InfoHash);
|
||||
}
|
||||
@@ -230,13 +230,13 @@ namespace NzbDrone.Core.History
|
||||
DownloadId = downloadId
|
||||
};
|
||||
|
||||
//Won't have a value since we publish this event before saving to DB.
|
||||
//history.Data.Add("FileId", message.ImportedEpisode.Id.ToString());
|
||||
history.Data.Add("FileId", message.ImportedBook.Id.ToString());
|
||||
history.Data.Add("DroppedPath", message.BookInfo.Path);
|
||||
history.Data.Add("ImportedPath", message.ImportedBook.Path);
|
||||
history.Data.Add("DownloadClient", message.DownloadClientInfo?.Type);
|
||||
history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name);
|
||||
history.Data.Add("ReleaseGroup", message.BookInfo.ReleaseGroup);
|
||||
history.Data.Add("Size", message.BookInfo.Size.ToString());
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
@@ -259,6 +259,7 @@ namespace NzbDrone.Core.History
|
||||
history.Data.Add("DownloadClient", message.DownloadClient);
|
||||
history.Data.Add("DownloadClientName", message.TrackedDownload?.DownloadItem.DownloadClientInfo.Name);
|
||||
history.Data.Add("Message", message.Message);
|
||||
history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString());
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
@@ -311,6 +312,7 @@ namespace NzbDrone.Core.History
|
||||
history.Data.Add("SourcePath", sourcePath);
|
||||
history.Data.Add("Path", path);
|
||||
history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup);
|
||||
history.Data.Add("Size", message.BookFile.Size.ToString());
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
@@ -362,8 +364,9 @@ namespace NzbDrone.Core.History
|
||||
};
|
||||
|
||||
history.Data.Add("DownloadClient", message.DownloadClientInfo.Name);
|
||||
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup);
|
||||
history.Data.Add("Message", message.Message);
|
||||
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup);
|
||||
history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString());
|
||||
|
||||
historyToAdd.Add(history);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace NzbDrone.Core.ImportLists.Readarr
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string ForeignBookId { get; set; }
|
||||
public string ForeignEditionId { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public List<MediaCover.MediaCover> Images { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
|
||||
@@ -68,10 +68,7 @@ namespace NzbDrone.Core.ImportLists.Readarr
|
||||
{
|
||||
BookGoodreadsId = remoteBook.ForeignBookId,
|
||||
Book = remoteBook.Title,
|
||||
|
||||
// ToDo: Fix me. Edition is no longer in the book resource; rethink edition logic
|
||||
// Bandaid fix for now...This will cause the imported book to possibly not be same edition as the source
|
||||
// EditionGoodreadsId = remoteBook.Editions.Single(x => x.Monitored).ForeignEditionId,
|
||||
EditionGoodreadsId = remoteBook.ForeignEditionId,
|
||||
Author = remoteAuthor.AuthorName,
|
||||
AuthorGoodreadsId = remoteAuthor.ForeignAuthorId
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user