mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-26 17:44:27 -04:00
Compare commits
78 Commits
v0.3.15.23
...
v0.3.20.24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3407cc9a7a | ||
|
|
4829916f0a | ||
|
|
c505eafd30 | ||
|
|
07f218f294 | ||
|
|
42751b598b | ||
|
|
5e7e0eb50b | ||
|
|
d6c631457c | ||
|
|
12ee76d222 | ||
|
|
3ea80038d3 | ||
|
|
55404cdf24 | ||
|
|
83a9cd4f3e | ||
|
|
3572d7330d | ||
|
|
a9b652a280 | ||
|
|
8efb2eb71a | ||
|
|
17094f1998 | ||
|
|
ddf5dc25a1 | ||
|
|
fa2614954b | ||
|
|
2e2894b3d3 | ||
|
|
59ff407e76 | ||
|
|
bbd7b9f92e | ||
|
|
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 |
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,14 +9,14 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.3.15'
|
||||
majorVersion: '0.3.20'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.417'
|
||||
nodeVersion: '16.X'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
@@ -218,10 +218,12 @@ class HistoryRow extends Component {
|
||||
key={name}
|
||||
className={styles.details}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
<div className={styles.actionContents}>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
</div>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -59,6 +59,7 @@ class BookRow extends Component {
|
||||
releaseDate,
|
||||
title,
|
||||
seriesTitle,
|
||||
authorName,
|
||||
position,
|
||||
pageCount,
|
||||
ratings,
|
||||
@@ -211,6 +212,7 @@ class BookRow extends Component {
|
||||
bookId={id}
|
||||
authorId={authorId}
|
||||
bookTitle={title}
|
||||
authorName={authorName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -229,6 +231,7 @@ BookRow.propTypes = {
|
||||
releaseDate: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
seriesTitle: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
position: PropTypes.string,
|
||||
pageCount: PropTypes.number,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
|
||||
@@ -33,6 +33,7 @@ function createMapStateToProps() {
|
||||
(author = {}, bookFiles, bookId) => {
|
||||
return {
|
||||
authorMonitored: author.monitored,
|
||||
authorName: author.authorName,
|
||||
bookFiles: bookFiles[bookId] ?? []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -38,6 +38,7 @@ class BookSearchCell extends Component {
|
||||
const {
|
||||
bookId,
|
||||
bookTitle,
|
||||
authorName,
|
||||
isSearching,
|
||||
onSearchPress,
|
||||
...otherProps
|
||||
@@ -60,6 +61,7 @@ class BookSearchCell extends Component {
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
bookId={bookId}
|
||||
bookTitle={bookTitle}
|
||||
authorName={authorName}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
{...otherProps}
|
||||
/>
|
||||
@@ -73,6 +75,7 @@ BookSearchCell.propTypes = {
|
||||
bookId: PropTypes.number.isRequired,
|
||||
authorId: PropTypes.number.isRequired,
|
||||
bookTitle: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
onSearchPress: PropTypes.func.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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,19 +9,21 @@ function BookInteractiveSearchModal(props) {
|
||||
isOpen,
|
||||
bookId,
|
||||
bookTitle,
|
||||
authorName,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<BookInteractiveSearchModalContent
|
||||
bookId={bookId}
|
||||
bookTitle={bookTitle}
|
||||
authorName={authorName}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
@@ -32,6 +34,7 @@ BookInteractiveSearchModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
bookId: PropTypes.number.isRequired,
|
||||
bookTitle: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -7,18 +7,23 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function BookInteractiveSearchModalContent(props) {
|
||||
const {
|
||||
bookId,
|
||||
bookTitle,
|
||||
authorName,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Interactive Search {bookId != null && `- ${bookTitle}`}
|
||||
{bookId === null ?
|
||||
translate('InteractiveSearchModalHeader') :
|
||||
translate('InteractiveSearchModalHeaderBookAuthor', { bookTitle, authorName })
|
||||
}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
@@ -32,7 +37,7 @@ function BookInteractiveSearchModalContent(props) {
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
@@ -42,6 +47,7 @@ function BookInteractiveSearchModalContent(props) {
|
||||
BookInteractiveSearchModalContent.propTypes = {
|
||||
bookId: PropTypes.number.isRequired,
|
||||
bookTitle: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { maxBy } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
|
||||
if (id) {
|
||||
dispatchSetFilter({ selectedFilterKey: id });
|
||||
} else {
|
||||
const last = customFilters[customFilters.length -1];
|
||||
const last = maxBy(customFilters, 'id');
|
||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||
}
|
||||
|
||||
@@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
labelErrors: [
|
||||
{
|
||||
message: 'Label is required'
|
||||
message: translate('LabelIsRequired')
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Custom Filter
|
||||
{translate('CustomFilter')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
Label
|
||||
{translate('Label')}
|
||||
</div>
|
||||
|
||||
<div className={styles.labelInputContainer}>
|
||||
@@ -195,7 +196,7 @@ class FilterBuilderModalContent extends Component {
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onCancelPress}>
|
||||
Cancel
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
@@ -203,7 +204,7 @@ class FilterBuilderModalContent extends Component {
|
||||
error={saveError}
|
||||
onPress={this.onSaveFilterPress}
|
||||
>
|
||||
Save
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
||||
dispatchSetFilter
|
||||
} = this.props;
|
||||
|
||||
// Assume that delete and then unmounting means the delete was successful.
|
||||
// Moving this check to a ancestor would be more accurate, but would have
|
||||
// Assume that delete and then unmounting means the deletion was successful.
|
||||
// Moving this check to an ancestor would be more accurate, but would have
|
||||
// more boilerplate.
|
||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -91,6 +91,7 @@ class TextTagInputConnector extends Component {
|
||||
render() {
|
||||
return (
|
||||
<TagInput
|
||||
delimiters={['Tab', 'Enter', ',']}
|
||||
tagList={[]}
|
||||
onTagAdd={this.onTagAdd}
|
||||
onTagDelete={this.onTagDelete}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -63,6 +63,13 @@
|
||||
width: 1280px;
|
||||
}
|
||||
|
||||
|
||||
.extraExtraLarge {
|
||||
composes: modal;
|
||||
|
||||
width: 1600px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||
.modal.extraLarge {
|
||||
width: 90%;
|
||||
@@ -90,7 +97,8 @@
|
||||
.modal.small,
|
||||
.modal.medium,
|
||||
.modal.large,
|
||||
.modal.extraLarge {
|
||||
.modal.extraLarge,
|
||||
.modal.extraExtraLarge {
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
|
||||
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'extraExtraLarge': string;
|
||||
'extraLarge': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
|
||||
@@ -3,5 +3,5 @@ export const SMALL = 'small';
|
||||
export const MEDIUM = 'medium';
|
||||
export const LARGE = 'large';
|
||||
export const EXTRA_LARGE = 'extraLarge';
|
||||
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
||||
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -48,7 +48,7 @@ class InteractiveImportModal extends Component {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.title {
|
||||
composes: cell;
|
||||
}
|
||||
|
||||
.title div {
|
||||
overflow-wrap: break-word;
|
||||
.titleContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
|
||||
@@ -11,7 +11,7 @@ interface CssExports {
|
||||
'quality': string;
|
||||
'rejected': string;
|
||||
'size': string;
|
||||
'title': string;
|
||||
'titleContent': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -153,10 +153,12 @@ class InteractiveSearchRow extends Component {
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.title}>
|
||||
<Link to={infoUrl}>
|
||||
{title}
|
||||
</Link>
|
||||
<TableRowCell>
|
||||
<div className={styles.titleContent}>
|
||||
<Link to={infoUrl}>
|
||||
{title}
|
||||
</Link>
|
||||
</div>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexer}>
|
||||
|
||||
@@ -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>' }} />
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||
@@ -173,6 +174,16 @@ class CutoffUnmet extends Component {
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
|
||||
@@ -84,6 +84,7 @@ function CutoffUnmetRow(props) {
|
||||
bookId={id}
|
||||
authorId={author.id}
|
||||
bookTitle={title}
|
||||
authorName={author.authorName}
|
||||
bookEntity={bookEntities.WANTED_CUTOFF_UNMET}
|
||||
showOpenAuthorButton={true}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
@@ -190,6 +191,16 @@ class Missing extends Component {
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
|
||||
@@ -84,6 +84,7 @@ function MissingRow(props) {
|
||||
bookId={id}
|
||||
authorId={author.id}
|
||||
bookTitle={title}
|
||||
authorName={author.authorName}
|
||||
bookEntity={bookEntities.WANTED_MISSING}
|
||||
showOpenAuthorButton={true}
|
||||
/>
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#3a3f51" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
<!-- Android/Apple Phone -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<meta name="description" content="Readarr">
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#3a3f51" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
<!-- Android/Apple Phone -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<meta name="description" content="Readarr" />
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
@@ -103,31 +105,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +251,18 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
||||
}
|
||||
|
||||
private static bool HasRoutableIPv4Address()
|
||||
{
|
||||
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
|
||||
return networkInterfaces.Any(ni =>
|
||||
ni.OperationalStatus == OperationalStatus.Up &&
|
||||
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip.Address)));
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
||||
@@ -265,10 +286,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
}
|
||||
catch
|
||||
{
|
||||
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
|
||||
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
|
||||
// but in the interest of keeping this implementation simple, this is acceptable.
|
||||
useIPv6 = false;
|
||||
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
|
||||
useIPv6 = !HasRoutableIPv4Address();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = "2024-01-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 = "2024-01-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]
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
ExceptionVerification.IgnoreWarns();
|
||||
}
|
||||
|
||||
[TestCase("Harry Potter and the sorcerer's stone a summary of the novel", 23314781)]
|
||||
[TestCase("Harry Potter and the sorcerer's stone a detailed summary", 61800696)]
|
||||
[TestCase("B0192CTMYG", 61209488)]
|
||||
[TestCase("9780439554930", 48517161)]
|
||||
public void successful_book_search(string title, int expected)
|
||||
|
||||
@@ -34,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; }
|
||||
@@ -90,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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,17 +42,18 @@ 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 =>
|
||||
{
|
||||
opt.PreviewOnly = false;
|
||||
opt.Timeout = TimeSpan.FromSeconds(60);
|
||||
opt.Timeout = TimeSpan.FromMinutes(5);
|
||||
})
|
||||
.Configure<SelectingProcessorAccessorOptions>(cfg =>
|
||||
{
|
||||
|
||||
@@ -132,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,
|
||||
|
||||
@@ -89,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,
|
||||
|
||||
@@ -135,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;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
@@ -101,11 +103,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
|
||||
public string AddTorrentFromMagnet(string magnetLink, DelugeSettings settings)
|
||||
{
|
||||
var options = new
|
||||
dynamic options = new ExpandoObject();
|
||||
|
||||
options.add_paused = settings.AddPaused;
|
||||
options.remove_at_ratio = false;
|
||||
|
||||
if (settings.DownloadDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
add_paused = settings.AddPaused,
|
||||
remove_at_ratio = false
|
||||
};
|
||||
options.download_location = settings.DownloadDirectory;
|
||||
}
|
||||
|
||||
if (settings.CompletedDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
options.move_completed_path = settings.CompletedDirectory;
|
||||
options.move_completed = true;
|
||||
}
|
||||
|
||||
var response = ProcessRequest<string>(settings, "core.add_torrent_magnet", magnetLink, options);
|
||||
|
||||
@@ -114,11 +126,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
|
||||
public string AddTorrentFromFile(string filename, byte[] fileContent, DelugeSettings settings)
|
||||
{
|
||||
var options = new
|
||||
dynamic options = new ExpandoObject();
|
||||
|
||||
options.add_paused = settings.AddPaused;
|
||||
options.remove_at_ratio = false;
|
||||
|
||||
if (settings.DownloadDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
add_paused = settings.AddPaused,
|
||||
remove_at_ratio = false
|
||||
};
|
||||
options.download_location = settings.DownloadDirectory;
|
||||
}
|
||||
|
||||
if (settings.CompletedDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
options.move_completed_path = settings.CompletedDirectory;
|
||||
options.move_completed = true;
|
||||
}
|
||||
|
||||
var response = ProcessRequest<string>(settings, "core.add_torrent_file", filename, fileContent, options);
|
||||
return response;
|
||||
|
||||
@@ -59,6 +59,12 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
[FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)]
|
||||
public bool AddPaused { get; set; }
|
||||
|
||||
[FieldDefinition(10, Label = "DownloadClientDelugeSettingsDirectory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryHelpText")]
|
||||
public string DownloadDirectory { get; set; }
|
||||
|
||||
[FieldDefinition(11, Label = "DownloadClientDelugeSettingsDirectoryCompleted", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryCompletedHelpText")]
|
||||
public string CompletedDirectory { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -89,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,
|
||||
|
||||
@@ -109,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)),
|
||||
|
||||
@@ -57,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,
|
||||
|
||||
|
||||
@@ -231,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
|
||||
@@ -613,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,
|
||||
@@ -203,11 +203,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
DeleteItemData(item);
|
||||
}
|
||||
|
||||
_proxy.RemoveFrom("history", item.DownloadId, deleteData, Settings);
|
||||
_proxy.RemoveFromHistory(item.DownloadId, deleteData, item.Status == DownloadItemStatus.Failed, Settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
_proxy.RemoveFrom("queue", item.DownloadId, deleteData, Settings);
|
||||
_proxy.RemoveFromQueue(item.DownloadId, deleteData, Settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
{
|
||||
string GetBaseUrl(SabnzbdSettings settings, string relativePath = null);
|
||||
SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings);
|
||||
void RemoveFrom(string source, string id, bool deleteData, SabnzbdSettings settings);
|
||||
void RemoveFromQueue(string id, bool deleteData, SabnzbdSettings settings);
|
||||
void RemoveFromHistory(string id, bool deleteData, bool deletePermanently, SabnzbdSettings settings);
|
||||
string GetVersion(SabnzbdSettings settings);
|
||||
SabnzbdConfig GetConfig(SabnzbdSettings settings);
|
||||
SabnzbdFullStatus GetFullStatus(SabnzbdSettings settings);
|
||||
@@ -60,9 +61,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
return response;
|
||||
}
|
||||
|
||||
public void RemoveFrom(string source, string id, bool deleteData, SabnzbdSettings settings)
|
||||
public void RemoveFromQueue(string id, bool deleteData, SabnzbdSettings settings)
|
||||
{
|
||||
var request = BuildRequest(source, settings);
|
||||
var request = BuildRequest("queue", settings);
|
||||
request.AddQueryParam("name", "delete");
|
||||
request.AddQueryParam("del_files", deleteData ? 1 : 0);
|
||||
request.AddQueryParam("value", id);
|
||||
@@ -70,6 +71,17 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void RemoveFromHistory(string id, bool deleteData, bool deletePermanently, SabnzbdSettings settings)
|
||||
{
|
||||
var request = BuildRequest("history", settings);
|
||||
request.AddQueryParam("name", "delete");
|
||||
request.AddQueryParam("del_files", deleteData ? 1 : 0);
|
||||
request.AddQueryParam("value", id);
|
||||
request.AddQueryParam("archive", deletePermanently ? 0 : 1);
|
||||
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public string GetVersion(SabnzbdSettings settings)
|
||||
{
|
||||
var request = BuildRequest("version", settings);
|
||||
|
||||
@@ -72,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;
|
||||
|
||||
@@ -146,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));
|
||||
|
||||
@@ -120,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Queue;
|
||||
@@ -39,20 +39,31 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task SearchForMissingBooks(List<Book> books, bool userInvokedSearch)
|
||||
private async Task SearchForBulkBooks(List<Book> books, bool userInvokedSearch)
|
||||
{
|
||||
_logger.ProgressInfo("Performing missing search for {0} books", books.Count);
|
||||
var downloadedCount = 0;
|
||||
|
||||
foreach (var book in books)
|
||||
foreach (var book in books.OrderBy(a => a.LastSearchTime ?? DateTime.MinValue))
|
||||
{
|
||||
var decisions = await _releaseSearchService.BookSearch(book.Id, false, userInvokedSearch, false);
|
||||
List<DownloadDecision> decisions;
|
||||
|
||||
try
|
||||
{
|
||||
decisions = await _releaseSearchService.BookSearch(book.Id, false, userInvokedSearch, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to search for book: [{0}]", book);
|
||||
continue;
|
||||
}
|
||||
|
||||
var processed = await _processDownloadDecisions.ProcessDecisions(decisions);
|
||||
|
||||
downloadedCount += processed.Grabbed.Count;
|
||||
}
|
||||
|
||||
_logger.ProgressInfo("Completed missing search for {0} books. {1} reports downloaded.", books.Count, downloadedCount);
|
||||
_logger.ProgressInfo("Completed search for {0} books. {1} reports downloaded.", books.Count, downloadedCount);
|
||||
}
|
||||
|
||||
public void Execute(BookSearchCommand message)
|
||||
@@ -104,17 +115,11 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
var queue = _queueService.GetQueue().Where(q => q.Book != null).Select(q => q.Book.Id);
|
||||
var missing = books.Where(e => !queue.Contains(e.Id)).ToList();
|
||||
|
||||
SearchForMissingBooks(missing, message.Trigger == CommandTrigger.Manual).GetAwaiter().GetResult();
|
||||
SearchForBulkBooks(missing, message.Trigger == CommandTrigger.Manual).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void Execute(CutoffUnmetBookSearchCommand message)
|
||||
{
|
||||
Expression<Func<Book, bool>> filterExpression;
|
||||
|
||||
filterExpression = v =>
|
||||
v.Monitored == true &&
|
||||
v.Author.Value.Monitored == true;
|
||||
|
||||
var pagingSpec = new PagingSpec<Book>
|
||||
{
|
||||
Page = 1,
|
||||
@@ -123,14 +128,14 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
SortKey = "Id"
|
||||
};
|
||||
|
||||
pagingSpec.FilterExpressions.Add(filterExpression);
|
||||
pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Author.Value.Monitored == true);
|
||||
|
||||
var books = _bookCutoffService.BooksWhereCutoffUnmet(pagingSpec).Records.ToList();
|
||||
|
||||
var queue = _queueService.GetQueue().Where(q => q.Book != null).Select(q => q.Book.Id);
|
||||
var missing = books.Where(e => !queue.Contains(e.Id)).ToList();
|
||||
var cutoffUnmet = books.Where(e => !queue.Contains(e.Id)).ToList();
|
||||
|
||||
SearchForMissingBooks(missing, message.Trigger == CommandTrigger.Manual).GetAwaiter().GetResult();
|
||||
SearchForBulkBooks(cutoffUnmet, message.Trigger == CommandTrigger.Manual).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,16 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
_logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
|
||||
|
||||
// Update the last search time for all albums if at least 1 indexer was searched.
|
||||
if (indexers.Any())
|
||||
{
|
||||
var lastSearchTime = DateTime.UtcNow;
|
||||
_logger.Debug("Setting last search time to: {0}", lastSearchTime);
|
||||
|
||||
criteriaBase.Books.ForEach(a => a.LastSearchTime = lastSearchTime);
|
||||
_bookService.UpdateLastSearchTime(criteriaBase.Books);
|
||||
}
|
||||
|
||||
return _makeDownloadDecision.GetSearchDecision(reports, criteriaBase).ToList();
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user