mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
60 Commits
v4.0.0.924
...
v4.0.1.111
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
913b845faa | ||
|
|
6e81517d51 | ||
|
|
34e74eecd7 | ||
|
|
895eccebc5 | ||
|
|
f722d49b3a | ||
|
|
cac97c057f | ||
|
|
63e132d257 | ||
|
|
6ab1d8e16b | ||
|
|
80630bf97f | ||
|
|
904285045b | ||
|
|
1006ec6b52 | ||
|
|
4cb1100704 | ||
|
|
745b92daf4 | ||
|
|
9eafdbd1af | ||
|
|
200396ef7a | ||
|
|
c5a724f14e | ||
|
|
42b11528b4 | ||
|
|
e2210228b3 | ||
|
|
ded7c3c6e2 | ||
|
|
e1c6722aad | ||
|
|
e17655c26a | ||
|
|
e66c628241 | ||
|
|
8f0514a91d | ||
|
|
d7aea82e45 | ||
|
|
19db75b36b | ||
|
|
11a18b534a | ||
|
|
70807a9dcf | ||
|
|
350600607d | ||
|
|
e9f0c96249 | ||
|
|
d9acbf5682 | ||
|
|
07cbd7c8d2 | ||
|
|
0ea189d03c | ||
|
|
9e3f9f9618 | ||
|
|
68c326ae27 | ||
|
|
46367d2023 | ||
|
|
b64c52a846 | ||
|
|
345854d0fe | ||
|
|
31baed4b2c | ||
|
|
7d0d503a5e | ||
|
|
9f50166fa6 | ||
|
|
3c1ca6ea4e | ||
|
|
3cd4c67ba1 | ||
|
|
fc3a2e9ab2 | ||
|
|
a71d40edba | ||
|
|
9ba5850fca | ||
|
|
0d06418194 | ||
|
|
f95dd00b51 | ||
|
|
271266b10a | ||
|
|
cab93249ec | ||
|
|
8921c5d7a0 | ||
|
|
dbbf1a7f58 | ||
|
|
69f99373e5 | ||
|
|
7be5732a3a | ||
|
|
e66ba84fc0 | ||
|
|
c0b30a5028 | ||
|
|
3cf4d2907e | ||
|
|
ae96ebca57 | ||
|
|
d336aaf3f0 | ||
|
|
ec40bc6eea | ||
|
|
75bb34afaa |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -17,7 +17,7 @@ env:
|
||||
FRAMEWORK: net6.0
|
||||
BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
SONARR_MAJOR_VERSION: 4
|
||||
VERSION: 4.0.0
|
||||
VERSION: 4.0.1
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
@@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
@@ -305,9 +305,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);
|
||||
@@ -315,7 +322,7 @@ class Queue extends Component {
|
||||
return !!(item && item.seriesId && item.episodeId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ class QueueRow extends Component {
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
@@ -420,6 +421,7 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!series}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
@@ -450,6 +452,7 @@ QueueRow.propTypes = {
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
|
||||
@@ -1,171 +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>
|
||||
{translate('RemoveQueueItem', { sourceTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
||||
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> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('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,174 +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('RemoveFromDownloadClientHelpTextWarning')}
|
||||
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('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
||||
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> :
|
||||
null
|
||||
}
|
||||
</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;
|
||||
@@ -7,6 +7,7 @@ import AppSectionState, {
|
||||
import Language from 'Language/Language';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
@@ -35,10 +36,15 @@ export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export interface ImportListOptionsSettingsAppState
|
||||
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
downloadClients: DownloadClientAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexers: IndexerAppState;
|
||||
@@ -46,6 +52,7 @@ interface SettingsAppState {
|
||||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
ui: UiSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
.expandContainer,
|
||||
.collapseContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -224,16 +224,19 @@ class CalendarEventGroup extends Component {
|
||||
</div>
|
||||
|
||||
{
|
||||
showEpisodeInformation &&
|
||||
showEpisodeInformation ?
|
||||
<Link
|
||||
className={styles.expandContainer}
|
||||
component="div"
|
||||
onPress={this.onExpandPress}
|
||||
>
|
||||
|
||||
<Icon
|
||||
name={icons.EXPAND}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
</Link> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ function createIsDownloadingSelector() {
|
||||
(state) => state.queue.details,
|
||||
(episodeIds, details) => {
|
||||
return details.items.some((item) => {
|
||||
return item.episode && episodeIds.includes(item.episode.id);
|
||||
return !!(item.episodeId && episodeIds.includes(item.episodeId));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -264,6 +264,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,
|
||||
|
||||
@@ -298,14 +298,20 @@ function InteractiveImportModalContent(
|
||||
return acc;
|
||||
}
|
||||
|
||||
const lastSelectedSeason = acc.lastSelectedSeason;
|
||||
|
||||
acc.seasonSelectDisabled ||= !item.series;
|
||||
acc.episodeSelectDisabled ||= !item.seasonNumber;
|
||||
acc.episodeSelectDisabled ||=
|
||||
item.seasonNumber === undefined ||
|
||||
(lastSelectedSeason >= 0 && item.seasonNumber !== lastSelectedSeason);
|
||||
acc.lastSelectedSeason = item.seasonNumber ?? -1;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
seasonSelectDisabled: false,
|
||||
episodeSelectDisabled: false,
|
||||
lastSelectedSeason: -1,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -428,14 +428,16 @@ class SeriesDetails extends Component {
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.FOLDER}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.path}>
|
||||
{path}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.FOLDER}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.path}>
|
||||
{path}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Tooltip
|
||||
@@ -444,16 +446,18 @@ class SeriesDetails extends Component {
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
@@ -470,32 +474,36 @@ class SeriesDetails extends Component {
|
||||
title={translate('QualityProfile')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.PROFILE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.PROFILE}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
@@ -503,14 +511,16 @@ class SeriesDetails extends Component {
|
||||
title={statusDetails.message}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={statusDetails.icon}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{statusDetails.title}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={statusDetails.icon}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{statusDetails.title}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
{
|
||||
@@ -520,14 +530,16 @@ class SeriesDetails extends Component {
|
||||
title={translate('Network')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.NETWORK}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{network}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.NETWORK}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{network}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
|
||||
@@ -537,14 +549,16 @@ class SeriesDetails extends Component {
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
||||
@@ -129,10 +129,8 @@ class SeriesDetailsSeason extends Component {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const expand = _.some(items, (item) => {
|
||||
return isAfter(item.airDateUtc) ||
|
||||
isAfter(item.airDateUtc, { days: -30 });
|
||||
});
|
||||
const expand = _.some(items, (item) => isAfter(item.airDateUtc) || isAfter(item.airDateUtc, { days: -30 })) ||
|
||||
items.every((item) => !item.airDateUtc);
|
||||
|
||||
onExpandPress(seasonNumber, expand && seasonNumber > 0);
|
||||
}
|
||||
|
||||
@@ -190,11 +190,15 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
|
||||
|
||||
if (isSmallScreen) {
|
||||
const padding = bodyPaddingSmallScreen - 5;
|
||||
const width = window.innerWidth - padding * 2;
|
||||
const height = window.innerHeight;
|
||||
|
||||
setSize({
|
||||
width: window.innerWidth - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
if (width !== size.width || height !== size.height) {
|
||||
setSize({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ function EditSpecificationModalContent(props) {
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
fields && fields.some((x) => x.label === translate('RegularExpression')) &&
|
||||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||
|
||||
@@ -10,6 +10,7 @@ import translate from 'Utilities/String/translate';
|
||||
import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
|
||||
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
||||
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
|
||||
import ImportListOptions from './Options/ImportListOptions';
|
||||
|
||||
class ImportListSettings extends Component {
|
||||
|
||||
@@ -19,7 +20,10 @@ class ImportListSettings extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._saveCallback = null;
|
||||
|
||||
this.state = {
|
||||
isSaving: false,
|
||||
hasPendingChanges: false,
|
||||
isManageImportListsOpen: false
|
||||
};
|
||||
@@ -28,6 +32,14 @@ class ImportListSettings extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
setChildSave = (saveCallback) => {
|
||||
this._saveCallback = saveCallback;
|
||||
};
|
||||
|
||||
onChildStateChange = (payload) => {
|
||||
this.setState(payload);
|
||||
};
|
||||
|
||||
setListOptionsRef = (ref) => {
|
||||
this._listOptions = ref;
|
||||
};
|
||||
@@ -47,7 +59,9 @@ class ImportListSettings extends Component {
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this._listOptions.getWrappedInstance().save();
|
||||
if (this._saveCallback) {
|
||||
this._saveCallback();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
@@ -93,6 +107,12 @@ class ImportListSettings extends Component {
|
||||
|
||||
<PageContentBody>
|
||||
<ImportListsConnector />
|
||||
|
||||
<ImportListOptions
|
||||
setChildSave={this.setChildSave}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
|
||||
<ImportListsExclusionsConnector />
|
||||
<ManageImportListsModal
|
||||
isOpen={isManageImportListsOpen}
|
||||
|
||||
148
frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx
Normal file
148
frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
fetchImportListOptions,
|
||||
saveImportListOptions,
|
||||
setImportListOptionsValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const SECTION = 'importListOptions';
|
||||
const cleanLibraryLevelOptions = [
|
||||
{ key: 'disabled', value: () => translate('Disabled') },
|
||||
{ key: 'logOnly', value: () => translate('LogOnly') },
|
||||
{ key: 'keepAndUnmonitor', value: () => translate('KeepAndUnmonitorSeries') },
|
||||
{ key: 'keepAndTag', value: () => translate('KeepAndTagSeries') },
|
||||
];
|
||||
|
||||
function createImportListOptionsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.advancedSettings,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
save: sectionSettings.isSaving,
|
||||
...sectionSettings,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface ImportListOptionsPageProps {
|
||||
setChildSave(saveCallback: () => void): void;
|
||||
onChildStateChange(payload: unknown): void;
|
||||
}
|
||||
|
||||
function ImportListOptions(props: ImportListOptionsPageProps) {
|
||||
const { setChildSave, onChildStateChange } = props;
|
||||
const selected = useSelector(createImportListOptionsSelector());
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
settings,
|
||||
hasSettings,
|
||||
} = selected;
|
||||
|
||||
const { listSyncLevel, listSyncTag } = settings;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }: { name: string; value: unknown }) => {
|
||||
// @ts-expect-error 'setImportListOptionsValue' isn't typed yet
|
||||
dispatch(setImportListOptionsValue({ name, value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onTagChange = useCallback(
|
||||
({ name, value }: { name: string; value: number[] }) => {
|
||||
const id = value.length === 0 ? 0 : value.pop();
|
||||
// @ts-expect-error 'setImportListOptionsValue' isn't typed yet
|
||||
dispatch(setImportListOptionsValue({ name, value: id }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchImportListOptions());
|
||||
setChildSave(() => dispatch(saveImportListOptions()));
|
||||
|
||||
return () => {
|
||||
dispatch(clearPendingChanges({ section: SECTION }));
|
||||
};
|
||||
}, [dispatch, setChildSave]);
|
||||
|
||||
useEffect(() => {
|
||||
onChildStateChange({
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
});
|
||||
}, [onChildStateChange, isSaving, hasPendingChanges]);
|
||||
|
||||
const translatedLevelOptions = cleanLibraryLevelOptions.map(
|
||||
({ key, value }) => {
|
||||
return {
|
||||
key,
|
||||
value: value(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return advancedSettings ? (
|
||||
<FieldSet legend={translate('Options')}>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<div>{translate('UnableToLoadListOptions')}</div>
|
||||
) : null}
|
||||
|
||||
{hasSettings && !isFetching && !error ? (
|
||||
<Form>
|
||||
<FormGroup advancedSettings={advancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('CleanLibraryLevel')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="listSyncLevel"
|
||||
values={translatedLevelOptions}
|
||||
helpText={translate('ListSyncLevelHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...listSyncLevel}
|
||||
/>
|
||||
</FormGroup>
|
||||
{listSyncLevel.value === 'keepAndTag' ? (
|
||||
<FormGroup advancedSettings={advancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('ListSyncTag')}</FormLabel>
|
||||
<FormInputGroup
|
||||
{...listSyncTag}
|
||||
type={inputTypes.TAG}
|
||||
name="listSyncTag"
|
||||
value={listSyncTag.value === 0 ? [] : [listSyncTag.value]}
|
||||
helpText={translate('ListSyncTagHelpText')}
|
||||
onChange={onTagChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
</Form>
|
||||
) : null}
|
||||
</FieldSet>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default ImportListOptions;
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.small {
|
||||
width: 480px;
|
||||
width: 490px;
|
||||
}
|
||||
|
||||
.large {
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
.token {
|
||||
flex: 0 0 50%;
|
||||
padding: 6px 16px;
|
||||
padding: 6px 6px;
|
||||
background-color: var(--popoverTitleBackgroundColor);
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 50%;
|
||||
padding: 6px 16px;
|
||||
padding: 6px 6px;
|
||||
background-color: var(--popoverBodyBackgroundColor);
|
||||
|
||||
.footNote {
|
||||
|
||||
@@ -191,7 +191,7 @@ class Notification extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ?
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ?
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
|
||||
@@ -15,12 +15,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
|
||||
@@ -61,7 +66,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 = {
|
||||
|
||||
@@ -88,7 +88,7 @@ function EditDelayProfileModalContent(props) {
|
||||
{
|
||||
!isFetching && !!error ?
|
||||
<div>
|
||||
{translate('AddQualityProfileError')}
|
||||
{translate('AddDelayProfileError')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -94,7 +94,12 @@ export default {
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.DESCENDING
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
|
||||
64
frontend/src/Store/Actions/Settings/importListOptions.js
Normal file
64
frontend/src/Store/Actions/Settings/importListOptions.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createSaveHandler from 'Store/Actions/Creators/createSaveHandler';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.importListOptions';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_IMPORT_LIST_OPTIONS = 'settings/importListOptions/fetchImportListOptions';
|
||||
export const SAVE_IMPORT_LIST_OPTIONS = 'settings/importListOptions/saveImportListOptions';
|
||||
export const SET_IMPORT_LIST_OPTIONS_VALUE = 'settings/importListOptions/setImportListOptionsValue';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchImportListOptions = createThunk(FETCH_IMPORT_LIST_OPTIONS);
|
||||
export const saveImportListOptions = createThunk(SAVE_IMPORT_LIST_OPTIONS);
|
||||
export const setImportListOptionsValue = createAction(SET_IMPORT_LIST_OPTIONS_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
pendingChanges: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
item: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_IMPORT_LIST_OPTIONS]: createFetchHandler(section, '/config/importlist'),
|
||||
[SAVE_IMPORT_LIST_OPTIONS]: createSaveHandler(section, '/config/importlist')
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_IMPORT_LIST_OPTIONS_VALUE]: createSetSettingValueReducer(section)
|
||||
}
|
||||
|
||||
};
|
||||
@@ -99,7 +99,12 @@ export default {
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.DESCENDING
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
@@ -149,7 +154,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;
|
||||
|
||||
@@ -430,13 +430,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;
|
||||
|
||||
@@ -454,7 +455,8 @@ export const actionHandlers = handleThunks({
|
||||
ids,
|
||||
remove,
|
||||
blocklist,
|
||||
skipRedownload
|
||||
skipRedownload,
|
||||
changeCategory
|
||||
} = payload;
|
||||
|
||||
dispatch(batchActions([
|
||||
@@ -470,7 +472,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',
|
||||
contentType: 'application/json',
|
||||
|
||||
@@ -10,6 +10,7 @@ import downloadClientOptions from './Settings/downloadClientOptions';
|
||||
import downloadClients from './Settings/downloadClients';
|
||||
import general from './Settings/general';
|
||||
import importListExclusions from './Settings/importListExclusions';
|
||||
import importListOptions from './Settings/importListOptions';
|
||||
import importLists from './Settings/importLists';
|
||||
import indexerOptions from './Settings/indexerOptions';
|
||||
import indexers from './Settings/indexers';
|
||||
@@ -33,6 +34,7 @@ export * from './Settings/delayProfiles';
|
||||
export * from './Settings/downloadClients';
|
||||
export * from './Settings/downloadClientOptions';
|
||||
export * from './Settings/general';
|
||||
export * from './Settings/importListOptions';
|
||||
export * from './Settings/importLists';
|
||||
export * from './Settings/importListExclusions';
|
||||
export * from './Settings/indexerOptions';
|
||||
@@ -69,6 +71,7 @@ export const defaultState = {
|
||||
general: general.defaultState,
|
||||
importLists: importLists.defaultState,
|
||||
importListExclusions: importListExclusions.defaultState,
|
||||
importListOptions: importListOptions.defaultState,
|
||||
indexerOptions: indexerOptions.defaultState,
|
||||
indexers: indexers.defaultState,
|
||||
languages: languages.defaultState,
|
||||
@@ -112,6 +115,7 @@ export const actionHandlers = handleThunks({
|
||||
...general.actionHandlers,
|
||||
...importLists.actionHandlers,
|
||||
...importListExclusions.actionHandlers,
|
||||
...importListOptions.actionHandlers,
|
||||
...indexerOptions.actionHandlers,
|
||||
...indexers.actionHandlers,
|
||||
...languages.actionHandlers,
|
||||
@@ -146,6 +150,7 @@ export const reducers = createHandleActions({
|
||||
...general.reducers,
|
||||
...importLists.reducers,
|
||||
...importListExclusions.reducers,
|
||||
...importListOptions.reducers,
|
||||
...indexerOptions.reducers,
|
||||
...indexers.reducers,
|
||||
...languages.reducers,
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
|
||||
function createSettingsSectionSelector(section) {
|
||||
return createSelector(
|
||||
(state) => state.settings[section],
|
||||
(sectionSettings) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
item,
|
||||
pendingChanges,
|
||||
isSaving,
|
||||
saveError
|
||||
} = sectionSettings;
|
||||
|
||||
const settings = selectSettings(item, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createSettingsSectionSelector;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppSectionState, {
|
||||
AppSectionItemState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import AppState from 'App/State/AppState';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { PendingSection } from 'typings/pending';
|
||||
|
||||
type SettingNames = keyof Omit<AppState['settings'], 'advancedSettings'>;
|
||||
type GetSectionState<Name extends SettingNames> = AppState['settings'][Name];
|
||||
type GetSettingsSectionItemType<Name extends SettingNames> =
|
||||
GetSectionState<Name> extends AppSectionItemState<infer R>
|
||||
? R
|
||||
: GetSectionState<Name> extends AppSectionState<infer R>
|
||||
? R
|
||||
: never;
|
||||
|
||||
type AppStateWithPending<Name extends SettingNames> = {
|
||||
item?: GetSettingsSectionItemType<Name>;
|
||||
pendingChanges?: Partial<GetSettingsSectionItemType<Name>>;
|
||||
saveError?: Error;
|
||||
} & GetSectionState<Name>;
|
||||
|
||||
function createSettingsSectionSelector<Name extends SettingNames>(
|
||||
section: Name
|
||||
) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings[section],
|
||||
(sectionSettings) => {
|
||||
const { item, pendingChanges, saveError, ...other } =
|
||||
sectionSettings as AppStateWithPending<Name>;
|
||||
|
||||
const { settings, ...rest } = selectSettings(
|
||||
item,
|
||||
pendingChanges,
|
||||
saveError
|
||||
);
|
||||
|
||||
return {
|
||||
...other,
|
||||
saveError,
|
||||
settings: settings as PendingSection<GetSettingsSectionItemType<Name>>,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createSettingsSectionSelector;
|
||||
@@ -24,6 +24,7 @@ class About extends Component {
|
||||
runtimeVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
migrationVersion,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -76,6 +77,11 @@ class About extends Component {
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('DatabaseMigration')}
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
@@ -117,6 +123,7 @@ About.propTypes = {
|
||||
isDocker: PropTypes.bool.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,
|
||||
|
||||
10
frontend/src/typings/ImportListOptionsSettings.ts
Normal file
10
frontend/src/typings/ImportListOptionsSettings.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type ListSyncLevel =
|
||||
| 'disabled'
|
||||
| 'logOnly'
|
||||
| 'keepAndUnmonitor'
|
||||
| 'keepAndTag';
|
||||
|
||||
export default interface ImportListOptionsSettings {
|
||||
listSyncLevel: ListSyncLevel;
|
||||
listSyncTag: number;
|
||||
}
|
||||
9
frontend/src/typings/pending.ts
Normal file
9
frontend/src/typings/pending.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface Pending<T> {
|
||||
value: T;
|
||||
errors: any[];
|
||||
warnings: any[];
|
||||
}
|
||||
|
||||
export type PendingSection<T> = {
|
||||
[K in keyof T]: Pending<T[K]>;
|
||||
};
|
||||
@@ -10,6 +10,16 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
|
||||
where TSubject : class, IDiskProvider
|
||||
{
|
||||
[Test]
|
||||
public void writealltext_should_truncate_existing()
|
||||
{
|
||||
var file = GetTempFilePath();
|
||||
|
||||
Subject.WriteAllText(file, "A pretty long string");
|
||||
Subject.WriteAllText(file, "A short string");
|
||||
Subject.ReadAllText(file).Should().Be("A short string");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Retry(5)]
|
||||
public void directory_exist_should_be_able_to_find_existing_folder()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Common.Test.ExtensionTests.StringExtensionTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ReverseFixture
|
||||
{
|
||||
[TestCase("input", "tupni")]
|
||||
[TestCase("racecar", "racecar")]
|
||||
public void should_reverse_string(string input, string expected)
|
||||
{
|
||||
input.Reverse().Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ namespace NzbDrone.Common.Disk
|
||||
{
|
||||
var testPath = Path.Combine(path, "sonarr_write_test.txt");
|
||||
var testContent = $"This file was created to verify if '{path}' is writable. It should've been automatically deleted. Feel free to delete it.";
|
||||
File.WriteAllText(testPath, testContent);
|
||||
WriteAllText(testPath, testContent);
|
||||
File.Delete(testPath);
|
||||
return true;
|
||||
}
|
||||
@@ -311,7 +311,16 @@ namespace NzbDrone.Common.Disk
|
||||
{
|
||||
Ensure.That(filename, () => filename).IsValidPath(PathValidationType.CurrentOs);
|
||||
RemoveReadOnly(filename);
|
||||
File.WriteAllText(filename, contents);
|
||||
|
||||
// File.WriteAllText is broken on net core when writing to some CIFS mounts
|
||||
// This workaround from https://github.com/dotnet/runtime/issues/42790#issuecomment-700362617
|
||||
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
using (var writer = new StreamWriter(fs))
|
||||
{
|
||||
writer.Write(contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void FolderSetLastWriteTime(string path, DateTime dateTime)
|
||||
|
||||
@@ -242,5 +242,14 @@ namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
return input.Contains(':') ? $"[{input}]" : input;
|
||||
}
|
||||
|
||||
public static string Reverse(this string text)
|
||||
{
|
||||
var array = text.ToCharArray();
|
||||
|
||||
Array.Reverse(array);
|
||||
|
||||
return new string(array);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification
|
||||
{
|
||||
[TestFixture]
|
||||
public class MultiLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification>
|
||||
{
|
||||
private CustomFormatInput _input;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_input = new CustomFormatInput
|
||||
{
|
||||
EpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build(),
|
||||
Series = Builder<Series>.CreateNew().With(s => s.OriginalLanguage = Language.English).Build(),
|
||||
Size = 100.Megabytes(),
|
||||
Languages = new List<Language>
|
||||
{
|
||||
Language.English,
|
||||
Language.French
|
||||
},
|
||||
Filename = "Series.Title.S01E01"
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_one_language()
|
||||
{
|
||||
Subject.Value = Language.French.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_different_language()
|
||||
{
|
||||
Subject.Value = Language.Spanish.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_negated_when_one_language_matches()
|
||||
{
|
||||
Subject.Value = Language.French.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_negated_when_all_languages_do_not_match()
|
||||
{
|
||||
Subject.Value = Language.Spanish.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification
|
||||
{
|
||||
[TestFixture]
|
||||
public class OriginalLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification>
|
||||
{
|
||||
private CustomFormatInput _input;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_input = new CustomFormatInput
|
||||
{
|
||||
EpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build(),
|
||||
Series = Builder<Series>.CreateNew().With(s => s.OriginalLanguage = Language.English).Build(),
|
||||
Size = 100.Megabytes(),
|
||||
Languages = new List<Language>
|
||||
{
|
||||
Language.French
|
||||
},
|
||||
Filename = "Series.Title.S01E01"
|
||||
};
|
||||
}
|
||||
|
||||
public void GivenLanguages(params Language[] languages)
|
||||
{
|
||||
_input.Languages = languages.ToList();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_same_single_language()
|
||||
{
|
||||
GivenLanguages(Language.English);
|
||||
|
||||
Subject.Value = Language.Original.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_different_single_language()
|
||||
{
|
||||
Subject.Value = Language.Original.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_negated_same_single_language()
|
||||
{
|
||||
GivenLanguages(Language.English);
|
||||
|
||||
Subject.Value = Language.Original.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_negated_different_single_language()
|
||||
{
|
||||
Subject.Value = Language.Original.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification
|
||||
{
|
||||
[TestFixture]
|
||||
public class SingleLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification>
|
||||
{
|
||||
private CustomFormatInput _input;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_input = new CustomFormatInput
|
||||
{
|
||||
EpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build(),
|
||||
Series = Builder<Series>.CreateNew().With(s => s.OriginalLanguage = Language.English).Build(),
|
||||
Size = 100.Megabytes(),
|
||||
Languages = new List<Language>
|
||||
{
|
||||
Language.French
|
||||
},
|
||||
Filename = "Series.Title.S01E01"
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_same_language()
|
||||
{
|
||||
Subject.Value = Language.French.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_different_language()
|
||||
{
|
||||
Subject.Value = Language.Spanish.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_negated_same_language()
|
||||
{
|
||||
Subject.Value = Language.French.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_negated_different_language()
|
||||
{
|
||||
Subject.Value = Language.Spanish.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class parse_title_from_existing_subtitle_filesFixture : MigrationTest<parse_title_from_existing_subtitle_files>
|
||||
{
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.eng.testtitle.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.fra.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.fra.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle - 3.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle - 3.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle - 3.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle - 3.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].3.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].3.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.3.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.3.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
|
||||
[TestCase("Name (2020) - Name.2020.S01E03.REAL.PROPER.1080p.HEVC.x265-MeGusta - 0609901d2ea34acd81c9030980406065.en.forced.srt", "Name (2020)/Season 1/Name (2020) - Name.2020.S01E03.REAL.PROPER.1080p.HEVC.x265-MeGusta - 0609901d2ea34acd81c9030980406065.mkv", null, 0)]
|
||||
public void should_process_file_with_missing_title(string subtitlePath, string episodePath, string title, int copy)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var db = WithDapperMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("SubtitleFiles").Row(new
|
||||
{
|
||||
SeriesId = 1,
|
||||
SeasonNumber = 1,
|
||||
EpisodeFileId = 1,
|
||||
RelativePath = subtitlePath,
|
||||
Added = now,
|
||||
LastUpdated = now,
|
||||
Extension = Path.GetExtension(subtitlePath),
|
||||
Language = 10,
|
||||
LanguageTags = new List<string> { "sdh" }.ToJson()
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("EpisodeFiles").Row(new
|
||||
{
|
||||
Id = 1,
|
||||
SeriesId = 1,
|
||||
RelativePath = episodePath,
|
||||
Quality = new { }.ToJson(),
|
||||
Size = 0,
|
||||
DateAdded = now,
|
||||
SeasonNumber = 1,
|
||||
Languages = new List<int> { 1 }.ToJson()
|
||||
});
|
||||
});
|
||||
|
||||
var files = db.Query<SubtitleFile198>("SELECT * FROM \"SubtitleFiles\"").ToList();
|
||||
|
||||
files.Should().HaveCount(1);
|
||||
|
||||
files.First().Title.Should().Be(title);
|
||||
files.First().Copy.Should().Be(copy);
|
||||
files.First().LanguageTags.Should().NotContain("sdh");
|
||||
files.First().Language.Should().NotBe(10);
|
||||
}
|
||||
}
|
||||
|
||||
public class SubtitleFile198
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int? EpisodeFileId { get; set; }
|
||||
public int? SeasonNumber { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public string Extension { get; set; }
|
||||
public int Language { get; set; }
|
||||
public int Copy { get; set; }
|
||||
public string Title { get; set; }
|
||||
public List<string> LanguageTags { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Notifications.Email;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class email_encryptionFixture : MigrationTest<email_encryption>
|
||||
{
|
||||
[Test]
|
||||
public void should_convert_do_not_require_encryption_to_auto()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Notifications").Row(new
|
||||
{
|
||||
OnGrab = true,
|
||||
OnDownload = true,
|
||||
OnUpgrade = true,
|
||||
OnHealthIssue = true,
|
||||
IncludeHealthWarnings = true,
|
||||
OnRename = true,
|
||||
Name = "Mail Sonarr",
|
||||
Implementation = "Email",
|
||||
Tags = "[]",
|
||||
Settings = new EmailSettings200
|
||||
{
|
||||
Server = "smtp.gmail.com",
|
||||
Port = 563,
|
||||
To = new List<string> { "dont@email.me" },
|
||||
RequireEncryption = false
|
||||
}.ToJson(),
|
||||
ConfigContract = "EmailSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<NotificationDefinition201>("SELECT * FROM \"Notifications\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Email");
|
||||
items.First().ConfigContract.Should().Be("EmailSettings");
|
||||
items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_convert_require_encryption_to_always()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Notifications").Row(new
|
||||
{
|
||||
OnGrab = true,
|
||||
OnDownload = true,
|
||||
OnUpgrade = true,
|
||||
OnHealthIssue = true,
|
||||
IncludeHealthWarnings = true,
|
||||
OnRename = true,
|
||||
Name = "Mail Sonarr",
|
||||
Implementation = "Email",
|
||||
Tags = "[]",
|
||||
Settings = new EmailSettings200
|
||||
{
|
||||
Server = "smtp.gmail.com",
|
||||
Port = 563,
|
||||
To = new List<string> { "dont@email.me" },
|
||||
RequireEncryption = true
|
||||
}.ToJson(),
|
||||
ConfigContract = "EmailSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<NotificationDefinition201>("SELECT * FROM \"Notifications\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Email");
|
||||
items.First().ConfigContract.Should().Be("EmailSettings");
|
||||
items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Always);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_defaults_when_settings_are_empty()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Notifications").Row(new
|
||||
{
|
||||
OnGrab = true,
|
||||
OnDownload = true,
|
||||
OnUpgrade = true,
|
||||
OnHealthIssue = true,
|
||||
IncludeHealthWarnings = true,
|
||||
OnRename = true,
|
||||
Name = "Mail Sonarr",
|
||||
Implementation = "Email",
|
||||
Tags = "[]",
|
||||
Settings = new { }.ToJson(),
|
||||
ConfigContract = "EmailSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<NotificationDefinition201>("SELECT * FROM \"Notifications\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Email");
|
||||
items.First().ConfigContract.Should().Be("EmailSettings");
|
||||
items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred);
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationDefinition201
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public EmailSettings201 Settings { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool OnGrab { get; set; }
|
||||
public bool OnDownload { get; set; }
|
||||
public bool OnUpgrade { get; set; }
|
||||
public bool OnRename { get; set; }
|
||||
public bool OnSeriesDelete { get; set; }
|
||||
public bool OnEpisodeFileDelete { get; set; }
|
||||
public bool OnEpisodeFileDeleteForUpgrade { get; set; }
|
||||
public bool OnHealthIssue { get; set; }
|
||||
public bool OnApplicationUpdate { get; set; }
|
||||
public bool OnManualInteractionRequired { get; set; }
|
||||
public bool OnSeriesAdd { get; set; }
|
||||
public bool OnHealthRestored { get; set; }
|
||||
public bool SupportsOnGrab { get; set; }
|
||||
public bool SupportsOnDownload { get; set; }
|
||||
public bool SupportsOnUpgrade { get; set; }
|
||||
public bool SupportsOnRename { get; set; }
|
||||
public bool SupportsOnSeriesDelete { get; set; }
|
||||
public bool SupportsOnEpisodeFileDelete { get; set; }
|
||||
public bool SupportsOnEpisodeFileDeleteForUpgrade { get; set; }
|
||||
public bool SupportsOnHealthIssue { get; set; }
|
||||
public bool IncludeHealthWarnings { get; set; }
|
||||
public List<int> Tags { get; set; }
|
||||
}
|
||||
|
||||
public class EmailSettings200
|
||||
{
|
||||
public string Server { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool RequireEncryption { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string From { get; set; }
|
||||
public IEnumerable<string> To { get; set; }
|
||||
public IEnumerable<string> Cc { get; set; }
|
||||
public IEnumerable<string> Bcc { get; set; }
|
||||
}
|
||||
|
||||
public class EmailSettings201
|
||||
{
|
||||
public string Server { get; set; }
|
||||
public int Port { get; set; }
|
||||
public int UseEncryption { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string From { get; set; }
|
||||
public IEnumerable<string> To { get; set; }
|
||||
public IEnumerable<string> Cc { get; set; }
|
||||
public IEnumerable<string> Bcc { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
public void should_return_ok_on_episode_imported_event()
|
||||
{
|
||||
GivenFolderExists(_downloadRootPath);
|
||||
var importEvent = new EpisodeImportedEvent(new LocalEpisode(), new EpisodeFile(), new List<EpisodeFile>(), true, new DownloadClientItem());
|
||||
var importEvent = new EpisodeImportedEvent(new LocalEpisode(), new EpisodeFile(), new List<DeletedEpisodeFile>(), true, new DownloadClientItem());
|
||||
|
||||
Subject.Check(importEvent).ShouldBeOk();
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Test.HistoryTests
|
||||
DownloadId = "abcd"
|
||||
};
|
||||
|
||||
Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, new List<EpisodeFile>(), true, downloadClientItem));
|
||||
Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, new List<DeletedEpisodeFile>(), true, downloadClientItem));
|
||||
|
||||
Mocker.GetMock<IHistoryRepository>()
|
||||
.Verify(v => v.Insert(It.Is<EpisodeHistory>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path))));
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.ImportListItems;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class FetchAndParseImportListServiceFixture : CoreTest<FetchAndParseImportListService>
|
||||
{
|
||||
private List<IImportList> _importLists;
|
||||
private List<ImportListItemInfo> _listSeries;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_importLists = new List<IImportList>();
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>()))
|
||||
.Returns(_importLists);
|
||||
|
||||
_listSeries = Builder<ImportListItemInfo>.CreateListOfSize(5)
|
||||
.Build().ToList();
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Setup(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>()))
|
||||
.Returns((string value) => new List<Tv.Series>() { new Tv.Series() { ImdbId = value } });
|
||||
}
|
||||
|
||||
private Mock<IImportList> WithList(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult, TimeSpan? minRefresh = null, int? lastSyncOffset = null, int? syncDeletedCount = null)
|
||||
{
|
||||
return CreateListResult(id, enabled, enabledAuto, fetchResult, minRefresh, lastSyncOffset, syncDeletedCount);
|
||||
}
|
||||
|
||||
private Mock<IImportList> CreateListResult(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult, TimeSpan? minRefresh = null, int? lastSyncOffset = null, int? syncDeletedCount = null)
|
||||
{
|
||||
var refreshInterval = minRefresh ?? TimeSpan.FromHours(12);
|
||||
var importListDefinition = new ImportListDefinition { Id = id, Enable = enabled, EnableAutomaticAdd = enabledAuto, MinRefreshInterval = refreshInterval };
|
||||
|
||||
var mockImportList = new Mock<IImportList>();
|
||||
mockImportList.SetupGet(s => s.Definition).Returns(importListDefinition);
|
||||
mockImportList.Setup(s => s.Fetch()).Returns(fetchResult);
|
||||
mockImportList.SetupGet(s => s.MinRefreshInterval).Returns(refreshInterval);
|
||||
|
||||
DateTime? lastSync = lastSyncOffset.HasValue ? DateTime.UtcNow.AddHours(lastSyncOffset.Value) : null;
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Setup(v => v.GetListStatus(id))
|
||||
.Returns(new ImportListStatus() { LastInfoSync = lastSync });
|
||||
|
||||
if (syncDeletedCount.HasValue)
|
||||
{
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Setup(v => v.SyncSeriesForList(It.IsAny<List<ImportListItemInfo>>(), id))
|
||||
.Returns(syncDeletedCount.Value);
|
||||
}
|
||||
|
||||
_importLists.Add(mockImportList.Object);
|
||||
|
||||
return mockImportList;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_recently_fetched_list()
|
||||
{
|
||||
var fetchResult = new ImportListFetchResult();
|
||||
var list = WithList(1, true, true, fetchResult, lastSyncOffset: 0);
|
||||
|
||||
var result = Subject.Fetch();
|
||||
|
||||
list.Verify(f => f.Fetch(), Times.Never());
|
||||
result.Series.Count.Should().Be(0);
|
||||
result.AnyFailure.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_recent_and_fetch_good()
|
||||
{
|
||||
var fetchResult = new ImportListFetchResult();
|
||||
var recent = WithList(1, true, true, fetchResult, lastSyncOffset: 0);
|
||||
var old = WithList(2, true, true, fetchResult);
|
||||
|
||||
var result = Subject.Fetch();
|
||||
|
||||
recent.Verify(f => f.Fetch(), Times.Never());
|
||||
old.Verify(f => f.Fetch(), Times.Once());
|
||||
result.AnyFailure.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_failure_if_single_list_fails()
|
||||
{
|
||||
var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = true };
|
||||
WithList(1, true, true, fetchResult);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(It.IsAny<int>(), It.IsAny<bool>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_failure_if_any_list_fails()
|
||||
{
|
||||
var fetchResult1 = new ImportListFetchResult { Series = _listSeries, AnyFailure = true };
|
||||
WithList(1, true, true, fetchResult1);
|
||||
var fetchResult2 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(2, true, true, fetchResult2);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_early_if_no_available_lists()
|
||||
{
|
||||
var listResult = Subject.Fetch();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.GetListStatus(It.IsAny<int>()), Times.Never());
|
||||
|
||||
listResult.Series.Count.Should().Be(0);
|
||||
listResult.AnyFailure.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_store_series_if_list_doesnt_fail()
|
||||
{
|
||||
var listId = 1;
|
||||
var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(listId, true, true, fetchResult);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeFalse();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(listId, false), Times.Once());
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Verify(v => v.SyncSeriesForList(_listSeries, listId), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_store_series_if_list_fails()
|
||||
{
|
||||
var listId = 1;
|
||||
var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = true };
|
||||
WithList(listId, true, true, fetchResult);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(listId, false), Times.Never());
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Verify(v => v.SyncSeriesForList(It.IsAny<List<ImportListItemInfo>>(), listId), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_only_store_series_for_lists_that_dont_fail()
|
||||
{
|
||||
var passedListId = 1;
|
||||
var fetchResult1 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(passedListId, true, true, fetchResult1);
|
||||
var failedListId = 2;
|
||||
var fetchResult2 = new ImportListFetchResult { Series = _listSeries, AnyFailure = true };
|
||||
WithList(failedListId, true, true, fetchResult2);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(passedListId, false), Times.Once());
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Verify(v => v.SyncSeriesForList(_listSeries, passedListId), Times.Once());
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(failedListId, false), Times.Never());
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Verify(v => v.SyncSeriesForList(It.IsAny<List<ImportListItemInfo>>(), failedListId), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_all_results_for_all_lists()
|
||||
{
|
||||
var passedListId = 1;
|
||||
var fetchResult1 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(passedListId, true, true, fetchResult1);
|
||||
var secondListId = 2;
|
||||
var fetchResult2 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(secondListId, true, true, fetchResult2);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeFalse();
|
||||
listResult.Series.Count.Should().Be(5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_removed_flag_if_list_has_removed_items()
|
||||
{
|
||||
var listId = 1;
|
||||
var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(listId, true, true, fetchResult, syncDeletedCount: 500);
|
||||
|
||||
var result = Subject.Fetch();
|
||||
result.AnyFailure.Should().BeFalse();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(listId, true), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.ImportLists.ImportListItems;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
public class ImportListItemServiceFixture : CoreTest<ImportListItemService>
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var existing = Builder<ImportListItemInfo>.CreateListOfSize(3)
|
||||
.TheFirst(1)
|
||||
.With(s => s.TvdbId = 6)
|
||||
.With(s => s.ImdbId = "6")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 7)
|
||||
.With(s => s.ImdbId = "7")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 8)
|
||||
.With(s => s.ImdbId = "8")
|
||||
.Build().ToList();
|
||||
Mocker.GetMock<IImportListItemInfoRepository>()
|
||||
.Setup(v => v.GetAllForLists(It.IsAny<List<int>>()))
|
||||
.Returns(existing);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_insert_new_update_existing_and_delete_missing()
|
||||
{
|
||||
var newItems = Builder<ImportListItemInfo>.CreateListOfSize(3)
|
||||
.TheFirst(1)
|
||||
.With(s => s.TvdbId = 5)
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 6)
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 7)
|
||||
.Build().ToList();
|
||||
|
||||
var numDeleted = Subject.SyncSeriesForList(newItems, 1);
|
||||
|
||||
numDeleted.Should().Be(1);
|
||||
Mocker.GetMock<IImportListItemInfoRepository>()
|
||||
.Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == 5)), Times.Once());
|
||||
Mocker.GetMock<IImportListItemInfoRepository>()
|
||||
.Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 2 && s[0].TvdbId == 6 && s[1].TvdbId == 7)), Times.Once());
|
||||
Mocker.GetMock<IImportListItemInfoRepository>()
|
||||
.Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == 8)), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.Exclusions;
|
||||
using NzbDrone.Core.ImportLists.ImportListItems;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -13,17 +17,61 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
public class ImportListSyncServiceFixture : CoreTest<ImportListSyncService>
|
||||
{
|
||||
private List<ImportListItemInfo> _importListReports;
|
||||
private ImportListFetchResult _importListFetch;
|
||||
private List<ImportListItemInfo> _list1Series;
|
||||
private List<ImportListItemInfo> _list2Series;
|
||||
|
||||
private List<Series> _existingSeries;
|
||||
private List<IImportList> _importLists;
|
||||
private ImportListSyncCommand _commandAll;
|
||||
private ImportListSyncCommand _commandSingle;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var importListItem1 = new ImportListItemInfo
|
||||
_importLists = new List<IImportList>();
|
||||
|
||||
var item1 = new ImportListItemInfo()
|
||||
{
|
||||
Title = "Breaking Bad"
|
||||
};
|
||||
|
||||
_importListReports = new List<ImportListItemInfo> { importListItem1 };
|
||||
_list1Series = new List<ImportListItemInfo>() { item1 };
|
||||
|
||||
_existingSeries = Builder<Series>.CreateListOfSize(3)
|
||||
.TheFirst(1)
|
||||
.With(s => s.TvdbId = 6)
|
||||
.With(s => s.ImdbId = "6")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 7)
|
||||
.With(s => s.ImdbId = "7")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 8)
|
||||
.With(s => s.ImdbId = "8")
|
||||
.Build().ToList();
|
||||
|
||||
_list2Series = Builder<ImportListItemInfo>.CreateListOfSize(3)
|
||||
.TheFirst(1)
|
||||
.With(s => s.TvdbId = 6)
|
||||
.With(s => s.ImdbId = "6")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 7)
|
||||
.With(s => s.ImdbId = "7")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 8)
|
||||
.With(s => s.ImdbId = "8")
|
||||
.Build().ToList();
|
||||
|
||||
_importListFetch = new ImportListFetchResult(_list1Series, false);
|
||||
|
||||
_commandAll = new ImportListSyncCommand
|
||||
{
|
||||
};
|
||||
|
||||
_commandSingle = new ImportListSyncCommand
|
||||
{
|
||||
DefinitionId = 1
|
||||
};
|
||||
|
||||
var mockImportList = new Mock<IImportList>();
|
||||
|
||||
@@ -31,6 +79,10 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
.Setup(v => v.AllSeriesTvdbIds())
|
||||
.Returns(new List<int>());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Setup(v => v.GetAllSeries())
|
||||
.Returns(_existingSeries);
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Setup(v => v.SearchForNewSeries(It.IsAny<string>()))
|
||||
.Returns(new List<Series>());
|
||||
@@ -41,15 +93,19 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<ImportListDefinition> { new ImportListDefinition { ShouldMonitor = MonitorTypes.All } });
|
||||
.Returns(() => _importLists.Select(x => x.Definition as ImportListDefinition).ToList());
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.GetAvailableProviders())
|
||||
.Returns(_importLists);
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>()))
|
||||
.Returns(new List<IImportList> { mockImportList.Object });
|
||||
.Returns(() => _importLists.Where(x => (x.Definition as ImportListDefinition).EnableAutomaticAdd).ToList());
|
||||
|
||||
Mocker.GetMock<IFetchAndParseImportList>()
|
||||
.Setup(v => v.Fetch())
|
||||
.Returns(_importListReports);
|
||||
.Returns(_importListFetch);
|
||||
|
||||
Mocker.GetMock<IImportListExclusionService>()
|
||||
.Setup(v => v.All())
|
||||
@@ -58,19 +114,19 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
|
||||
private void WithTvdbId()
|
||||
{
|
||||
_importListReports.First().TvdbId = 81189;
|
||||
_list1Series.First().TvdbId = 81189;
|
||||
}
|
||||
|
||||
private void WithImdbId()
|
||||
{
|
||||
_importListReports.First().ImdbId = "tt0496424";
|
||||
_list1Series.First().ImdbId = "tt0496424";
|
||||
}
|
||||
|
||||
private void WithExistingSeries()
|
||||
{
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Setup(v => v.AllSeriesTvdbIds())
|
||||
.Returns(new List<int> { _importListReports.First().TvdbId });
|
||||
.Returns(new List<int> { _list1Series.First().TvdbId });
|
||||
}
|
||||
|
||||
private void WithExcludedSeries()
|
||||
@@ -81,32 +137,282 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
new ImportListExclusion
|
||||
{
|
||||
TvdbId = 81189
|
||||
TvdbId = _list1Series.First().TvdbId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void WithMonitorType(MonitorTypes monitor)
|
||||
{
|
||||
_importLists.ForEach(li => (li.Definition as ImportListDefinition).ShouldMonitor = monitor);
|
||||
}
|
||||
|
||||
private void WithCleanLevel(ListSyncLevelType cleanLevel, int? tagId = null)
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.SetupGet(v => v.ListSyncLevel)
|
||||
.Returns(cleanLevel);
|
||||
if (tagId.HasValue)
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.SetupGet(v => v.ListSyncTag)
|
||||
.Returns(tagId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void WithList(int id, bool enabledAuto, int lastSyncHoursOffset = 0, bool pendingRemovals = true, DateTime? disabledTill = null)
|
||||
{
|
||||
var importListDefinition = new ImportListDefinition { Id = id, EnableAutomaticAdd = enabledAuto };
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<ImportListDefinition> { new ImportListDefinition { ShouldMonitor = monitor } });
|
||||
.Setup(v => v.Get(id))
|
||||
.Returns(importListDefinition);
|
||||
|
||||
var mockImportList = new Mock<IImportList>();
|
||||
mockImportList.SetupGet(s => s.Definition).Returns(importListDefinition);
|
||||
mockImportList.SetupGet(s => s.MinRefreshInterval).Returns(TimeSpan.FromHours(12));
|
||||
|
||||
var status = new ImportListStatus()
|
||||
{
|
||||
LastInfoSync = DateTime.UtcNow.AddHours(lastSyncHoursOffset),
|
||||
HasRemovedItemSinceLastClean = pendingRemovals,
|
||||
DisabledTill = disabledTill
|
||||
};
|
||||
|
||||
if (disabledTill.HasValue)
|
||||
{
|
||||
_importListFetch.AnyFailure = true;
|
||||
}
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Setup(v => v.GetListStatus(id))
|
||||
.Returns(status);
|
||||
|
||||
_importLists.Add(mockImportList.Object);
|
||||
}
|
||||
|
||||
private void VerifyDidAddTag(int expectedSeriesCount, int expectedTagId)
|
||||
{
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<List<Series>>(x => x.Count == expectedSeriesCount && x.All(series => series.Tags.Contains(expectedTagId))), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_if_series_title_and_no_series_id()
|
||||
public void should_not_clean_library_if_lists_have_not_removed_any_items()
|
||||
{
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
_importListFetch.Series = _existingSeries.Select(x => new ImportListItemInfo() { TvdbId = x.TvdbId }).ToList();
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true, pendingRemovals: false);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor);
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Once());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.IsAny<List<Series>>(), true), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_clean_library_if_config_value_disable()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(new List<Series>(), true), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_log_only_on_clean_library_if_config_value_logonly()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.LogOnly);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Once());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(new List<Series>(), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_unmonitor_on_clean_library_if_config_value_keepAndUnmonitor()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor);
|
||||
var monitored = _existingSeries.Count(x => x.Monitored);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Once());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count == monitored && s.All(m => !m.Monitored)), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_clean_on_clean_library_if_tvdb_match()
|
||||
{
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Setup(v => v.Exists(6, It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_clean_on_clean_library_if_imdb_match()
|
||||
{
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
|
||||
var x = _importLists;
|
||||
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Setup(v => v.Exists(It.IsAny<int>(), "6"))
|
||||
.Returns(true);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_tag_series_on_clean_library_if_config_value_keepAndTag()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndTag, 1);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Once());
|
||||
|
||||
VerifyDidAddTag(_existingSeries.Count, 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_clean_if_list_failures()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true, disabledTill: DateTime.UtcNow.AddHours(1));
|
||||
WithCleanLevel(ListSyncLevelType.LogOnly);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.IsAny<List<Series>>(), It.IsAny<bool>()), Times.Never());
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_new_series_from_single_list_to_library()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 1), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_new_series_from_multiple_list_to_library()
|
||||
{
|
||||
_list2Series.ForEach(m => m.ImportListId = 2);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
_importListFetch.Series.AddRange(_list2Series);
|
||||
|
||||
WithList(1, true);
|
||||
WithList(2, true);
|
||||
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 4), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_new_series_to_library_only_from_enabled_lists()
|
||||
{
|
||||
_list2Series.ForEach(m => m.ImportListId = 2);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
_importListFetch.Series.AddRange(_list2Series);
|
||||
|
||||
WithList(1, true);
|
||||
WithList(2, false);
|
||||
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 1), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_add_duplicate_series_from_seperate_lists()
|
||||
{
|
||||
_list2Series.ForEach(m => m.ImportListId = 2);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
_importListFetch.Series.AddRange(_list2Series);
|
||||
_importListFetch.Series[0].TvdbId = 6;
|
||||
|
||||
WithList(1, true);
|
||||
WithList(2, true);
|
||||
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 3), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_search_if_series_title_and_series_id()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithTvdbId();
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Never());
|
||||
@@ -115,8 +421,10 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
[Test]
|
||||
public void should_search_by_imdb_if_series_title_and_series_imdb()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithImdbId();
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Verify(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>()), Times.Once());
|
||||
@@ -125,10 +433,12 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
[Test]
|
||||
public void should_not_add_if_existing_series()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithTvdbId();
|
||||
WithExistingSeries();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>()));
|
||||
@@ -138,10 +448,12 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
[TestCase(MonitorTypes.All, true)]
|
||||
public void should_add_if_not_existing_series(MonitorTypes monitor, bool expectedSeriesMonitored)
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithTvdbId();
|
||||
WithMonitorType(monitor);
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 1 && t.First().Monitored == expectedSeriesMonitored), It.IsAny<bool>()));
|
||||
@@ -150,10 +462,12 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
[Test]
|
||||
public void should_not_add_if_excluded_series()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithTvdbId();
|
||||
WithExcludedSeries();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>()));
|
||||
@@ -177,7 +491,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
Mocker.GetMock<IFetchAndParseImportList>()
|
||||
.Setup(v => v.Fetch())
|
||||
.Returns(new List<ImportListItemInfo>());
|
||||
.Returns(new ImportListFetchResult());
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Path.Combine(_series.Path, "Scenes", "file6.mkv").AsOsAgnostic(),
|
||||
Path.Combine(_series.Path, "Shorts", "file7.mkv").AsOsAgnostic(),
|
||||
Path.Combine(_series.Path, "Trailers", "file8.mkv").AsOsAgnostic(),
|
||||
Path.Combine(_series.Path, "Other", "file9.mkv").AsOsAgnostic(),
|
||||
Path.Combine(_series.Path, "Series Title S01E01 (1080p BluRay x265 10bit Tigole).mkv").AsOsAgnostic(),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
||||
{
|
||||
[TestFixture]
|
||||
public class AggregateSubtitleInfoFixture : CoreTest<AggregateSubtitleInfo>
|
||||
{
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
|
||||
public void should_do_basic_parse(string relativePath, string originalFilePath, string path)
|
||||
{
|
||||
var episodeFile = new EpisodeFile
|
||||
{
|
||||
RelativePath = relativePath,
|
||||
OriginalFilePath = originalFilePath
|
||||
};
|
||||
|
||||
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
|
||||
|
||||
subtitleTitleInfo.Title.Should().BeNull();
|
||||
subtitleTitleInfo.Copy.Should().Be(0);
|
||||
}
|
||||
|
||||
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
|
||||
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
|
||||
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass")]
|
||||
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].testtitle.eng.default.ass")]
|
||||
public void should_not_parse_default(string relativePath, string path)
|
||||
{
|
||||
var episodeFile = new EpisodeFile
|
||||
{
|
||||
RelativePath = relativePath
|
||||
};
|
||||
|
||||
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
|
||||
|
||||
subtitleTitleInfo.LanguageTags.Should().NotContain("default");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.NotificationTests.EmailTests
|
||||
_emailSettings = Builder<EmailSettings>.CreateNew()
|
||||
.With(s => s.Server = "someserver")
|
||||
.With(s => s.Port = 567)
|
||||
.With(s => s.RequireEncryption = true)
|
||||
.With(s => s.UseEncryption = (int)EmailEncryptionType.Always)
|
||||
.With(s => s.From = "dont@email.me")
|
||||
.With(s => s.To = new string[] { "dont@email.me" })
|
||||
.Build();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
@@ -33,16 +33,16 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
RelativePath = "file1.S01E01E02.mkv"
|
||||
},
|
||||
|
||||
OldFiles = new List<EpisodeFile>
|
||||
OldFiles = new List<DeletedEpisodeFile>
|
||||
{
|
||||
new EpisodeFile
|
||||
new DeletedEpisodeFile(new EpisodeFile
|
||||
{
|
||||
RelativePath = "file1.S01E01.mkv"
|
||||
},
|
||||
new EpisodeFile
|
||||
}, null),
|
||||
new DeletedEpisodeFile(new EpisodeFile
|
||||
{
|
||||
RelativePath = "file1.S01E02.mkv"
|
||||
}
|
||||
}, null)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc
|
||||
_downloadMessage = Builder<DownloadMessage>.CreateNew()
|
||||
.With(d => d.Series = series)
|
||||
.With(d => d.EpisodeFile = episodeFile)
|
||||
.With(d => d.OldFiles = new List<EpisodeFile>())
|
||||
.With(d => d.OldFiles = new List<DeletedEpisodeFile>())
|
||||
.Build();
|
||||
|
||||
Subject.Definition = new NotificationDefinition();
|
||||
@@ -40,9 +40,12 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc
|
||||
|
||||
private void GivenOldFiles()
|
||||
{
|
||||
_downloadMessage.OldFiles = Builder<EpisodeFile>.CreateListOfSize(1)
|
||||
.Build()
|
||||
.ToList();
|
||||
_downloadMessage.OldFiles = Builder<DeletedEpisodeFile>
|
||||
.CreateListOfSize(1)
|
||||
.All()
|
||||
.WithFactory(() => new DeletedEpisodeFile(Builder<EpisodeFile>.CreateNew().Build(), null))
|
||||
.Build()
|
||||
.ToList();
|
||||
|
||||
Subject.Definition.Settings = new XbmcSettings
|
||||
{
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class TruncatedReleaseGroupFixture : CoreTest<FileNameBuilder>
|
||||
{
|
||||
private Series _series;
|
||||
private List<Episode> _episodes;
|
||||
private EpisodeFile _episodeFile;
|
||||
private NamingConfig _namingConfig;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_series = Builder<Series>
|
||||
.CreateNew()
|
||||
.With(s => s.Title = "Series Title")
|
||||
.Build();
|
||||
|
||||
_namingConfig = NamingConfig.Default;
|
||||
_namingConfig.MultiEpisodeStyle = 0;
|
||||
_namingConfig.RenameEpisodes = true;
|
||||
|
||||
Mocker.GetMock<INamingConfigService>()
|
||||
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||
|
||||
_episodes = new List<Episode>
|
||||
{
|
||||
Builder<Episode>.CreateNew()
|
||||
.With(e => e.Title = "Episode Title 1")
|
||||
.With(e => e.SeasonNumber = 1)
|
||||
.With(e => e.EpisodeNumber = 1)
|
||||
.Build()
|
||||
};
|
||||
|
||||
_episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" };
|
||||
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
private void GivenProper()
|
||||
{
|
||||
_episodeFile.Quality.Revision.Version = 2;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_truncate_from_beginning()
|
||||
{
|
||||
_series.Title = "The Fantastic Life of Mr. Sisko";
|
||||
|
||||
_episodeFile.Quality.Quality = Quality.Bluray1080p;
|
||||
_episodeFile.ReleaseGroup = "IWishIWasALittleBitTallerIWishIWasABallerIWishIHadAGirlWhoLookedGoodIWouldCallHerIWishIHadARabbitInAHatWithABatAndASixFourImpala";
|
||||
_episodes = _episodes.Take(1).ToList();
|
||||
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}-{ReleaseGroup:12}";
|
||||
|
||||
var result = Subject.BuildFileName(_episodes, _series, _episodeFile, ".mkv");
|
||||
result.Length.Should().BeLessOrEqualTo(255);
|
||||
result.Should().Be("The Fantastic Life of Mr. Sisko - S01E01 - Episode Title 1 Bluray-1080p-IWishIWas....mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_truncate_from_from_end()
|
||||
{
|
||||
_series.Title = "The Fantastic Life of Mr. Sisko";
|
||||
|
||||
_episodeFile.Quality.Quality = Quality.Bluray1080p;
|
||||
_episodeFile.ReleaseGroup = "IWishIWasALittleBitTallerIWishIWasABallerIWishIHadAGirlWhoLookedGoodIWouldCallHerIWishIHadARabbitInAHatWithABatAndASixFourImpala";
|
||||
_episodes = _episodes.Take(1).ToList();
|
||||
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}-{ReleaseGroup:-17}";
|
||||
|
||||
var result = Subject.BuildFileName(_episodes, _series, _episodeFile, ".mkv");
|
||||
result.Length.Should().BeLessOrEqualTo(255);
|
||||
result.Should().Be("The Fantastic Life of Mr. Sisko - S01E01 - Episode Title 1 Bluray-1080p-...ASixFourImpala.mkv");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class TruncatedSeriesTitleFixture : CoreTest<FileNameBuilder>
|
||||
{
|
||||
private Series _series;
|
||||
private NamingConfig _namingConfig;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_series = Builder<Series>
|
||||
.CreateNew()
|
||||
.With(s => s.Title = "Series Title")
|
||||
.Build();
|
||||
|
||||
_namingConfig = NamingConfig.Default;
|
||||
_namingConfig.MultiEpisodeStyle = 0;
|
||||
_namingConfig.RenameEpisodes = true;
|
||||
|
||||
Mocker.GetMock<INamingConfigService>()
|
||||
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
[TestCase("{Series Title:16}", "The Fantastic...")]
|
||||
[TestCase("{Series TitleThe:17}", "Fantastic Life...")]
|
||||
[TestCase("{Series CleanTitle:-13}", "...Mr. Sisko")]
|
||||
public void should_truncate_series_title(string format, string expected)
|
||||
{
|
||||
_series.Title = "The Fantastic Life of Mr. Sisko";
|
||||
_namingConfig.SeriesFolderFormat = format;
|
||||
|
||||
var result = Subject.GetSeriesFolder(_series, _namingConfig);
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,10 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Different show 1. Bölüm (23.10.2023) 720p WebDL AAC H.264 - TURG", "Different show", 1, 0, 0)]
|
||||
[TestCase("Dubbed show 79.BLM Sezon Finali(25.06.2023) 720p WEB-DL AAC2.0 H.264-TURG", "Dubbed show", 79, 0, 0)]
|
||||
[TestCase("Exclusive BLM Documentary with no false positives EP03.1080p.AAC.x264", "Exclusive BLM Documentary with no false positives", 3, 0, 0)]
|
||||
[TestCase("[SubsPlease] Title de Series S2 - 03 (540p) [63501322]", "Title de Series S2", 3, 0, 0)]
|
||||
[TestCase("[Naruto-Kun.Hu] Dr Series S3 - 21 [1080p]", "Dr Series S3", 21, 0, 0)]
|
||||
[TestCase("[Naruto-Kun.Hu] Series Title - 12 [1080p].mkv", "Series Title", 12, 0, 0)]
|
||||
[TestCase("[Naruto-Kun.Hu] Anime Triangle - 08 [1080p].mkv", "Anime Triangle", 8, 0, 0)]
|
||||
|
||||
// [TestCase("", "", 0, 0, 0)]
|
||||
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
|
||||
|
||||
@@ -428,5 +428,38 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
result.Languages.Should().Contain(Language.Original);
|
||||
result.Languages.Should().Contain(Language.English);
|
||||
}
|
||||
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.eng.ass", new[] { "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle.ass", new[] { "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.eng.testtitle.ass", new[] { "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.fra.forced.ass", new[] { "default", "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.fra.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", new[] { "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", new[] { "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", new[] { "forced" }, "testtitle", "French")]
|
||||
public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage)
|
||||
{
|
||||
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle);
|
||||
|
||||
subtitleTitleInfo.LanguageTags.Should().BeEquivalentTo(expectedTags);
|
||||
subtitleTitleInfo.Title.Should().BeEquivalentTo(expectedTitle);
|
||||
subtitleTitleInfo.Language.Should().BeEquivalentTo((Language)expectedLanguage);
|
||||
}
|
||||
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.forced.ass")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.ass")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ass")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.ass")]
|
||||
public void should_not_parse_false_title(string postTitle)
|
||||
{
|
||||
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle);
|
||||
subtitleTitleInfo.Language.Should().Be(Language.Unknown);
|
||||
subtitleTitleInfo.LanguageTags.Should().BeEmpty();
|
||||
subtitleTitleInfo.RawTitle.Should().BeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Series T Se.3 afl.3 en 4", "Series T", 3, new[] { 3, 4 })]
|
||||
[TestCase("Series Title (S15E06-08) City Sushi", "Series Title", 15, new[] { 6, 7, 8 })]
|
||||
[TestCase("Босх: Спадок (S2E1-4) / Series: Legacy (S2E1-4) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })]
|
||||
[TestCase("Босх: Спадок / Series: Legacy / S2E1-4 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })]
|
||||
|
||||
// [TestCase("", "", , new [] { })]
|
||||
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
|
||||
|
||||
@@ -94,6 +94,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
}
|
||||
|
||||
[TestCase("Босх: Спадок (S2E1) / Series: Legacy (S2E1) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Босх: Спадок", "Series: Legacy")]
|
||||
[TestCase("Босх: Спадок / Series: Legacy / S2E1-4 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Босх: Спадок", "Series: Legacy")]
|
||||
public void should_parse_multiple_series_titles(string postTitle, params string[] titles)
|
||||
{
|
||||
var seriesTitleInfo = Parser.Parser.ParseTitle(postTitle).SeriesTitleInfo;
|
||||
|
||||
@@ -83,6 +83,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Series Title (2012) - S01E01 - Episode 1 (1080p BluRay x265 r00t).mkv", "r00t")]
|
||||
[TestCase("Series Title - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")]
|
||||
[TestCase("Series.Title.S01E02.1080p.BluRay.Remux.AVC.FLAC.2.0-E.N.D", "E.N.D")]
|
||||
[TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")]
|
||||
public void should_parse_exception_release_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||
|
||||
@@ -165,6 +165,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Series Title [HDTV][Cap.402](website.com).avi", "Series Title", 4, 2)]
|
||||
[TestCase("Series Title [HDTV 720p][Cap.101](website.com).mkv", "Series Title", 1, 1)]
|
||||
[TestCase("Босх: Спадок (S2E1) / Series: Legacy (S2E1) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, 1)]
|
||||
[TestCase("Босх: Спадок / Series: Legacy / S2E1 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, 1)]
|
||||
|
||||
// [TestCase("", "", 0, 0)]
|
||||
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
|
||||
@@ -211,5 +212,19 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
result.FullSeason.Should().BeFalse();
|
||||
result.Special.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("Series.Title.S06E01b.Fade.Out.Fade.in.Part.2.1080p.DSNP.WEB-DL.AAC2.0.H.264-FLUX", "Series Title", 6, 1)]
|
||||
public void should_parse_split_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Should().NotBeNull();
|
||||
result.EpisodeNumbers.Should().HaveCount(1);
|
||||
result.SeasonNumber.Should().Be(seasonNumber);
|
||||
result.EpisodeNumbers.First().Should().Be(episodeNumber);
|
||||
result.SeriesTitle.Should().Be(title);
|
||||
result.AbsoluteEpisodeNumbers.Should().BeEmpty();
|
||||
result.FullSeason.Should().BeFalse();
|
||||
result.IsSplitEpisode.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||
@@ -276,6 +277,18 @@ namespace NzbDrone.Core.Configuration
|
||||
set { SetValue("ChownGroup", value); }
|
||||
}
|
||||
|
||||
public ListSyncLevelType ListSyncLevel
|
||||
{
|
||||
get { return GetValueEnum("ListSyncLevel", ListSyncLevelType.Disabled); }
|
||||
set { SetValue("ListSyncLevel", value); }
|
||||
}
|
||||
|
||||
public int ListSyncTag
|
||||
{
|
||||
get { return GetValueInt("ListSyncTag"); }
|
||||
set { SetValue("ListSyncTag", value); }
|
||||
}
|
||||
|
||||
public int FirstDayOfWeek
|
||||
{
|
||||
get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -52,6 +53,9 @@ namespace NzbDrone.Core.Configuration
|
||||
int MaximumSize { get; set; }
|
||||
int MinimumAge { get; set; }
|
||||
|
||||
ListSyncLevelType ListSyncLevel { get; set; }
|
||||
int ListSyncTag { get; set; }
|
||||
|
||||
// UI
|
||||
int FirstDayOfWeek { get; set; }
|
||||
string CalendarWeekColumnHeader { get; set; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.History;
|
||||
@@ -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(RemoteEpisode remoteEpisode, long size)
|
||||
@@ -153,20 +156,23 @@ namespace NzbDrone.Core.CustomFormats
|
||||
return matches.OrderBy(x => x.Name).ToList();
|
||||
}
|
||||
|
||||
private static List<CustomFormat> ParseCustomFormat(EpisodeFile episodeFile, Series series, List<CustomFormat> allCustomFormats)
|
||||
private List<CustomFormat> ParseCustomFormat(EpisodeFile episodeFile, Series series, List<CustomFormat> allCustomFormats)
|
||||
{
|
||||
var releaseTitle = string.Empty;
|
||||
|
||||
if (episodeFile.SceneName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Trace("Using scene name for release title: {0}", episodeFile.SceneName);
|
||||
releaseTitle = episodeFile.SceneName;
|
||||
}
|
||||
else if (episodeFile.OriginalFilePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Trace("Using original file path for release title: {0}", Path.GetFileName(episodeFile.OriginalFilePath));
|
||||
releaseTitle = Path.GetFileName(episodeFile.OriginalFilePath);
|
||||
}
|
||||
else if (episodeFile.RelativePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Trace("Using relative path for release title: {0}", Path.GetFileName(episodeFile.RelativePath));
|
||||
releaseTitle = Path.GetFileName(episodeFile.RelativePath);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats
|
||||
|
||||
public abstract NzbDroneValidationResult Validate();
|
||||
|
||||
public bool IsSatisfiedBy(CustomFormatInput input)
|
||||
public virtual bool IsSatisfiedBy(CustomFormatInput input)
|
||||
{
|
||||
var match = IsSatisfiedByWithoutNegate(input);
|
||||
|
||||
|
||||
@@ -30,6 +30,16 @@ namespace NzbDrone.Core.CustomFormats
|
||||
[FieldDefinition(1, Label = "CustomFormatsSpecificationLanguage", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))]
|
||||
public int Value { get; set; }
|
||||
|
||||
public override bool IsSatisfiedBy(CustomFormatInput input)
|
||||
{
|
||||
if (Negate)
|
||||
{
|
||||
return IsSatisfiedByWithNegate(input);
|
||||
}
|
||||
|
||||
return IsSatisfiedByWithoutNegate(input);
|
||||
}
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||
{
|
||||
var comparedLanguage = input.EpisodeInfo != null && input.Series != null && Value == Language.Original.Id && input.Series.OriginalLanguage != Language.Unknown
|
||||
@@ -39,6 +49,15 @@ namespace NzbDrone.Core.CustomFormats
|
||||
return input.Languages?.Contains(comparedLanguage) ?? false;
|
||||
}
|
||||
|
||||
private bool IsSatisfiedByWithNegate(CustomFormatInput input)
|
||||
{
|
||||
var comparedLanguage = input.EpisodeInfo != null && input.Series != null && Value == Language.Original.Id && input.Series.OriginalLanguage != Language.Unknown
|
||||
? input.Series.OriginalLanguage
|
||||
: (Language)Value;
|
||||
|
||||
return !input.Languages?.Contains(comparedLanguage) ?? false;
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(193)]
|
||||
public class add_import_list_items : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Create.TableForModel("ImportListItems")
|
||||
.WithColumn("ImportListId").AsInt32()
|
||||
.WithColumn("Title").AsString()
|
||||
.WithColumn("TvdbId").AsInt32()
|
||||
.WithColumn("Year").AsInt32().Nullable()
|
||||
.WithColumn("TmdbId").AsInt32().Nullable()
|
||||
.WithColumn("ImdbId").AsString().Nullable()
|
||||
.WithColumn("MalId").AsInt32().Nullable()
|
||||
.WithColumn("AniListId").AsInt32().Nullable()
|
||||
.WithColumn("ReleaseDate").AsDateTimeOffset().Nullable();
|
||||
|
||||
Alter.Table("ImportListStatus")
|
||||
.AddColumn("HasRemovedItemSinceLastClean").AsBoolean().WithDefaultValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(198)]
|
||||
public class parse_title_from_existing_subtitle_files : NzbDroneMigrationBase
|
||||
{
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(AggregateSubtitleInfo));
|
||||
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("SubtitleFiles").AddColumn("Title").AsString().Nullable();
|
||||
Alter.Table("SubtitleFiles").AddColumn("Copy").AsInt32().WithDefaultValue(0);
|
||||
Execute.WithConnection(UpdateTitles);
|
||||
}
|
||||
|
||||
private void UpdateTitles(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updates = new List<object>();
|
||||
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = "SELECT \"SubtitleFiles\".\"Id\", \"SubtitleFiles\".\"RelativePath\", \"EpisodeFiles\".\"RelativePath\", \"EpisodeFiles\".\"OriginalFilePath\" FROM \"SubtitleFiles\" JOIN \"EpisodeFiles\" ON \"SubtitleFiles\".\"EpisodeFileId\" = \"EpisodeFiles\".\"Id\"";
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var relativePath = reader.GetString(1);
|
||||
var episodeFileRelativePath = reader.GetString(2);
|
||||
var episodeFileOriginalFilePath = reader[3] as string;
|
||||
|
||||
var subtitleTitleInfo = CleanSubtitleTitleInfo(episodeFileRelativePath, episodeFileOriginalFilePath, relativePath);
|
||||
|
||||
updates.Add(new
|
||||
{
|
||||
Id = id,
|
||||
Title = subtitleTitleInfo.Title,
|
||||
Language = subtitleTitleInfo.Language,
|
||||
LanguageTags = subtitleTitleInfo.LanguageTags,
|
||||
Copy = subtitleTitleInfo.Copy
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var updateSubtitleFilesSql = "UPDATE \"SubtitleFiles\" SET \"Title\" = @Title, \"Copy\" = @Copy, \"Language\" = @Language, \"LanguageTags\" = @LanguageTags, \"LastUpdated\" = CURRENT_TIMESTAMP WHERE \"Id\" = @Id";
|
||||
conn.Execute(updateSubtitleFilesSql, updates, transaction: tran);
|
||||
}
|
||||
|
||||
private static SubtitleTitleInfo CleanSubtitleTitleInfo(string relativePath, string originalFilePath, string path)
|
||||
{
|
||||
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(path);
|
||||
|
||||
var episodeFileTitle = Path.GetFileNameWithoutExtension(relativePath);
|
||||
var originalEpisodeFileTitle = Path.GetFileNameWithoutExtension(originalFilePath) ?? string.Empty;
|
||||
|
||||
if (subtitleTitleInfo.TitleFirst && (episodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase) || originalEpisodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Logger.Debug("Subtitle title '{0}' is in episode file title '{1}'. Removing from subtitle title.", subtitleTitleInfo.RawTitle, episodeFileTitle);
|
||||
|
||||
subtitleTitleInfo = LanguageParser.ParseBasicSubtitle(path);
|
||||
}
|
||||
|
||||
var cleanedTags = subtitleTitleInfo.LanguageTags.Where(t => !episodeFileTitle.Contains(t, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
if (cleanedTags.Count != subtitleTitleInfo.LanguageTags.Count)
|
||||
{
|
||||
Logger.Debug("Removed language tags '{0}' from subtitle title '{1}'.", string.Join(", ", subtitleTitleInfo.LanguageTags.Except(cleanedTags)), subtitleTitleInfo.RawTitle);
|
||||
subtitleTitleInfo.LanguageTags = cleanedTags;
|
||||
}
|
||||
|
||||
return subtitleTitleInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(201)]
|
||||
public class email_encryption : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.WithConnection(ChangeEncryption);
|
||||
}
|
||||
|
||||
private void ChangeEncryption(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updated = new List<object>();
|
||||
using (var getEmailCmd = conn.CreateCommand())
|
||||
{
|
||||
getEmailCmd.Transaction = tran;
|
||||
getEmailCmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Email'";
|
||||
|
||||
using (var reader = getEmailCmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var settings = Json.Deserialize<JObject>(reader.GetString(1));
|
||||
|
||||
settings["useEncryption"] = settings.Value<bool>("requireEncryption") ? 1 : 0;
|
||||
settings["requireEncryption"] = null;
|
||||
|
||||
updated.Add(new
|
||||
{
|
||||
Settings = settings.ToJson(),
|
||||
Id = id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updateSql = "UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id";
|
||||
conn.Execute(updateSql, updated, transaction: tran);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,12 +42,13 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
serviceProvider = new ServiceCollection()
|
||||
.AddLogging(b => b.AddNLog())
|
||||
.AddFluentMigratorCore()
|
||||
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
|
||||
.ConfigureRunner(
|
||||
builder => builder
|
||||
.AddPostgres()
|
||||
.AddNzbDroneSQLite()
|
||||
.WithGlobalConnectionString(connectionString)
|
||||
.WithMigrationsIn(Assembly.GetExecutingAssembly()))
|
||||
.ScanIn(Assembly.GetExecutingAssembly()).For.All())
|
||||
.Configure<TypeFilterOptions>(opt => opt.Namespace = "NzbDrone.Core.Datastore.Migration")
|
||||
.Configure<ProcessorOptions>(opt =>
|
||||
{
|
||||
|
||||
@@ -81,6 +81,9 @@ namespace NzbDrone.Core.Datastore
|
||||
.Ignore(i => i.MinRefreshInterval)
|
||||
.Ignore(i => i.Enable);
|
||||
|
||||
Mapper.Entity<ImportListItemInfo>("ImportListItems").RegisterModel()
|
||||
.Ignore(i => i.ImportList);
|
||||
|
||||
Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel()
|
||||
.Ignore(x => x.ImplementationName)
|
||||
.Ignore(i => i.SupportsOnGrab)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class SplitEpisodeSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SplitEpisodeSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (subject.ParsedEpisodeInfo.IsSplitEpisode)
|
||||
{
|
||||
_logger.Debug("Split episode release {0} rejected. Not supported", subject.Release.Title);
|
||||
return Decision.Reject("Split episode releases are not supported");
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,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,
|
||||
|
||||
@@ -91,7 +91,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 = "sonarr",
|
||||
Title = item.Title,
|
||||
|
||||
@@ -61,7 +61,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 = "sonarr",
|
||||
Title = item.Title,
|
||||
|
||||
@@ -137,7 +137,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
item.Title = torrent.Name;
|
||||
item.Category = Settings.TvCategory;
|
||||
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace());
|
||||
|
||||
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath));
|
||||
item.OutputPath = outputPath + torrent.Name;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
var item = new DownloadClientItem()
|
||||
{
|
||||
Category = Settings.TvCategory,
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = CreateDownloadId(torrent.Id, serialNumber),
|
||||
Title = torrent.Title,
|
||||
TotalSize = torrent.Size,
|
||||
@@ -310,40 +310,35 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
try
|
||||
{
|
||||
var downloadDir = GetDefaultDir();
|
||||
var downloadDir = GetDownloadDirectory();
|
||||
|
||||
if (downloadDir == null)
|
||||
{
|
||||
return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), "DownloadClientDownloadStationValidationNoDefaultDestination")
|
||||
return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestination"))
|
||||
{
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestinationDetail", new Dictionary<string, object> { { "username", Settings.Username } })
|
||||
};
|
||||
}
|
||||
|
||||
downloadDir = GetDownloadDirectory();
|
||||
var sharedFolder = downloadDir.Split('\\', '/')[0];
|
||||
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
|
||||
|
||||
if (downloadDir != null)
|
||||
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
|
||||
|
||||
if (folderInfo.Additional == null)
|
||||
{
|
||||
var sharedFolder = downloadDir.Split('\\', '/')[0];
|
||||
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
|
||||
|
||||
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
|
||||
|
||||
if (folderInfo.Additional == null)
|
||||
return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing"))
|
||||
{
|
||||
return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing"))
|
||||
{
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail", new Dictionary<string, object> { { "sharedFolder", sharedFolder } })
|
||||
};
|
||||
}
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail", new Dictionary<string, object> { { "sharedFolder", sharedFolder } })
|
||||
};
|
||||
}
|
||||
|
||||
if (!folderInfo.IsDir)
|
||||
if (!folderInfo.IsDir)
|
||||
{
|
||||
return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing"))
|
||||
{
|
||||
return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing"))
|
||||
{
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary<string, object> { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } })
|
||||
};
|
||||
}
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary<string, object> { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } })
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -460,7 +455,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
|
||||
var destDir = GetDefaultDir();
|
||||
|
||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
if (destDir.IsNotNullOrWhiteSpace() && Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
var item = new DownloadClientItem()
|
||||
{
|
||||
Category = Settings.TvCategory,
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
DownloadId = CreateDownloadId(nzb.Id, serialNumber),
|
||||
Title = nzb.Title,
|
||||
TotalSize = nzb.Size,
|
||||
@@ -211,40 +211,36 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
try
|
||||
{
|
||||
var downloadDir = GetDefaultDir();
|
||||
var downloadDir = GetDownloadDirectory();
|
||||
|
||||
if (downloadDir == null)
|
||||
{
|
||||
return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), "DownloadClientDownloadStationValidationNoDefaultDestination")
|
||||
return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestination"))
|
||||
{
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestinationDetail", new Dictionary<string, object> { { "username", Settings.Username } })
|
||||
};
|
||||
}
|
||||
|
||||
downloadDir = GetDownloadDirectory();
|
||||
var sharedFolder = downloadDir.Split('\\', '/')[0];
|
||||
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
|
||||
|
||||
if (downloadDir != null)
|
||||
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
|
||||
|
||||
if (folderInfo.Additional == null)
|
||||
{
|
||||
var sharedFolder = downloadDir.Split('\\', '/')[0];
|
||||
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
|
||||
|
||||
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
|
||||
|
||||
if (folderInfo.Additional == null)
|
||||
return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing"))
|
||||
{
|
||||
return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing"))
|
||||
{
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail", new Dictionary<string, object> { { "sharedFolder", sharedFolder } })
|
||||
};
|
||||
}
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail",
|
||||
new Dictionary<string, object> { { "sharedFolder", sharedFolder } })
|
||||
};
|
||||
}
|
||||
|
||||
if (!folderInfo.IsDir)
|
||||
if (!folderInfo.IsDir)
|
||||
{
|
||||
return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing"))
|
||||
{
|
||||
return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing"))
|
||||
{
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary<string, object> { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } })
|
||||
};
|
||||
}
|
||||
DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary<string, object> { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } })
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -439,7 +435,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
|
||||
var destDir = GetDefaultDir();
|
||||
|
||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
if (destDir.IsNotNullOrWhiteSpace() && Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
|
||||
}
|
||||
|
||||
@@ -118,7 +118,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)),
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
Category = Settings.Category,
|
||||
Title = torrent.Name,
|
||||
TotalSize = torrent.Size,
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||
RemainingSize = (long)(torrent.Size * (double)(1 - ((double)torrent.ReceivedPrct / 10000))),
|
||||
RemainingTime = torrent.Eta <= 0 ? null : TimeSpan.FromSeconds(torrent.Eta),
|
||||
SeedRatio = torrent.StopRatio <= 0 ? 0 : torrent.StopRatio / 100,
|
||||
|
||||
@@ -59,7 +59,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,
|
||||
|
||||
@@ -58,7 +58,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;
|
||||
|
||||
@@ -73,7 +73,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;
|
||||
|
||||
@@ -120,7 +120,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);
|
||||
|
||||
@@ -75,7 +75,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.TvImportedCategory.IsNotNullOrWhiteSpace()),
|
||||
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
|
||||
RemainingTime = GetRemainingTime(torrent),
|
||||
SeedRatio = torrent.Ratio
|
||||
|
||||
@@ -65,7 +65,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;
|
||||
@@ -120,7 +120,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,
|
||||
@@ -278,7 +278,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);
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
item.Category = Settings.TvCategory;
|
||||
item.Title = torrent.Name;
|
||||
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
|
||||
|
||||
item.OutputPath = GetOutputPath(outputPath, torrent);
|
||||
item.TotalSize = torrent.TotalSize;
|
||||
|
||||
@@ -45,11 +45,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
[FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Transmission")]
|
||||
public bool UseSsl { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, eg http://[host]:[port]/[urlBase]/rpc, defaults to '/transmission/'")]
|
||||
[FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsUrlBaseHelpText")]
|
||||
[FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Transmission")]
|
||||
[FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/rpc")]
|
||||
[FieldToken(TokenField.HelpText, "UrlBase", "defaultUrl", "/transmission/")]
|
||||
|
||||
public string UrlBase { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
}
|
||||
|
||||
var item = new DownloadClientItem();
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
|
||||
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace());
|
||||
item.Title = torrent.Name;
|
||||
item.DownloadId = torrent.Hash;
|
||||
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path));
|
||||
|
||||
@@ -122,7 +122,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.TvImportedCategory.IsNotNullOrWhiteSpace());
|
||||
item.RemainingSize = torrent.Remaining;
|
||||
item.SeedRatio = torrent.Ratio;
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@ namespace NzbDrone.Core.Download
|
||||
if (series == null)
|
||||
{
|
||||
trackedDownload.Warn("Series title mismatch; automatic import is not possible. Check the download troubleshooting entry on the wiki for common causes.");
|
||||
SendManualInteractionRequiredNotification(trackedDownload);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,16 +108,7 @@ namespace NzbDrone.Core.Download
|
||||
if (seriesMatchType == SeriesMatchType.Id && releaseSource != ReleaseSourceType.InteractiveSearch)
|
||||
{
|
||||
trackedDownload.Warn("Found matching series via grab history, but release was matched to series by ID. Automatic import is not possible. See the FAQ for details.");
|
||||
|
||||
if (!trackedDownload.HasNotifiedManualInteractionRequired)
|
||||
{
|
||||
trackedDownload.HasNotifiedManualInteractionRequired = true;
|
||||
|
||||
var releaseInfo = new GrabbedReleaseInfo(grabbedHistories);
|
||||
var manualInteractionEvent = new ManualInteractionRequiredEvent(trackedDownload, releaseInfo);
|
||||
|
||||
_eventAggregator.PublishEvent(manualInteractionEvent);
|
||||
}
|
||||
SendManualInteractionRequiredNotification(trackedDownload);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -136,6 +129,8 @@ namespace NzbDrone.Core.Download
|
||||
if (trackedDownload.RemoteEpisode == null)
|
||||
{
|
||||
trackedDownload.Warn("Unable to parse download, automatic import is not possible.");
|
||||
SendManualInteractionRequiredNotification(trackedDownload);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,6 +187,7 @@ namespace NzbDrone.Core.Download
|
||||
if (statusMessages.Any())
|
||||
{
|
||||
trackedDownload.Warn(statusMessages.ToArray());
|
||||
SendManualInteractionRequiredNotification(trackedDownload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,6 +254,21 @@ namespace NzbDrone.Core.Download
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SendManualInteractionRequiredNotification(TrackedDownload trackedDownload)
|
||||
{
|
||||
if (!trackedDownload.HasNotifiedManualInteractionRequired)
|
||||
{
|
||||
var grabbedHistories = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId).Where(h => h.EventType == EpisodeHistoryEventType.Grabbed).ToList();
|
||||
|
||||
trackedDownload.HasNotifiedManualInteractionRequired = true;
|
||||
|
||||
var releaseInfo = grabbedHistories.Count > 0 ? new GrabbedReleaseInfo(grabbedHistories) : null;
|
||||
var manualInteractionEvent = new ManualInteractionRequiredEvent(trackedDownload, releaseInfo);
|
||||
|
||||
_eventAggregator.PublishEvent(manualInteractionEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetImportItem(TrackedDownload trackedDownload)
|
||||
{
|
||||
trackedDownload.ImportItem = _provideImportItemService.ProvideImportItem(trackedDownload.DownloadItem, trackedDownload.ImportItem);
|
||||
|
||||
@@ -37,9 +37,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)
|
||||
DownloadClientBase<TSettings> downloadClient, bool hasPostImportCategory)
|
||||
where TSettings : IProviderConfig, new()
|
||||
{
|
||||
return new DownloadClientItemClientInfo
|
||||
@@ -47,7 +48,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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ namespace NzbDrone.Core.Download.Pending
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly IRemoteEpisodeAggregationService _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,
|
||||
IRemoteEpisodeAggregationService 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.RemoteEpisode, 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.RemoteEpisode, matchingReport.Reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("The release {0} is already pending with reason {1}, changing to {2}", decision.RemoteEpisode, matchingReport.Reason, reason);
|
||||
matchingReport.Reason = reason;
|
||||
_repository.Update(matchingReport);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -355,6 +368,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, episode),
|
||||
@@ -371,7 +394,8 @@ namespace NzbDrone.Core.Download.Pending
|
||||
Added = pendingRelease.Added,
|
||||
Status = pendingRelease.Reason.ToString(),
|
||||
Protocol = pendingRelease.RemoteEpisode.Release.DownloadProtocol,
|
||||
Indexer = pendingRelease.RemoteEpisode.Release.Indexer
|
||||
Indexer = pendingRelease.RemoteEpisode.Release.Indexer,
|
||||
DownloadClient = downloadClientName
|
||||
};
|
||||
|
||||
return queue;
|
||||
|
||||
@@ -196,31 +196,30 @@ namespace NzbDrone.Core.Download
|
||||
private async Task<ProcessedDecisionResult> ProcessDecisionInternal(DownloadDecision decision, int? downloadClientId = null)
|
||||
{
|
||||
var remoteEpisode = decision.RemoteEpisode;
|
||||
var remoteIndexer = remoteEpisode.Release.Indexer;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Trace("Grabbing from Indexer {0} at priority {1}.", remoteEpisode.Release.Indexer, remoteEpisode.Release.IndexerPriority);
|
||||
_logger.Trace("Grabbing release '{0}' from Indexer {1} at priority {2}.", remoteEpisode, remoteIndexer, remoteEpisode.Release.IndexerPriority);
|
||||
await _downloadService.DownloadReport(remoteEpisode, downloadClientId);
|
||||
|
||||
return ProcessedDecisionResult.Grabbed;
|
||||
}
|
||||
catch (ReleaseUnavailableException)
|
||||
{
|
||||
_logger.Warn("Failed to download release from indexer, no longer available. " + remoteEpisode);
|
||||
_logger.Warn("Failed to download release '{0}' from Indexer {1}. Release not available", remoteEpisode, remoteIndexer);
|
||||
return ProcessedDecisionResult.Rejected;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is DownloadClientUnavailableException || ex is DownloadClientAuthenticationException)
|
||||
{
|
||||
_logger.Debug(ex,
|
||||
"Failed to send release to download client, storing until later. " + remoteEpisode);
|
||||
|
||||
_logger.Debug(ex, "Failed to send release '{0}' from Indexer {1} to download client, storing until later.", remoteEpisode, remoteIndexer);
|
||||
return ProcessedDecisionResult.Failed;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn(ex, "Couldn't add report to download queue. " + remoteEpisode);
|
||||
_logger.Warn(ex, "Couldn't add release '{0}' from Indexer {1} to download queue.", remoteEpisode, remoteIndexer);
|
||||
return ProcessedDecisionResult.Skipped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
@@ -71,15 +70,19 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
continue;
|
||||
}
|
||||
|
||||
var firstEpisode = localEpisode.Episodes.First();
|
||||
|
||||
var subtitleFile = new SubtitleFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
SeasonNumber = localEpisode.SeasonNumber,
|
||||
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId,
|
||||
EpisodeFileId = firstEpisode.EpisodeFileId,
|
||||
RelativePath = series.Path.GetRelativePath(possibleSubtitleFile),
|
||||
Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile),
|
||||
LanguageTags = LanguageParser.ParseLanguageTags(possibleSubtitleFile),
|
||||
Extension = extension
|
||||
Language = localEpisode.SubtitleInfo.Language,
|
||||
LanguageTags = localEpisode.SubtitleInfo.LanguageTags,
|
||||
Title = localEpisode.SubtitleInfo.Title,
|
||||
Extension = extension,
|
||||
Copy = localEpisode.SubtitleInfo.Copy
|
||||
};
|
||||
|
||||
subtitleFiles.Add(subtitleFile);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
||||
@@ -13,15 +14,40 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
|
||||
public Language Language { get; set; }
|
||||
|
||||
public string AggregateString => Language + LanguageTagsAsString + Extension;
|
||||
public string AggregateString => Language + Title + LanguageTagsAsString + Extension;
|
||||
|
||||
public int Copy { get; set; }
|
||||
|
||||
public List<string> LanguageTags { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
private string LanguageTagsAsString => string.Join(".", LanguageTags);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Id}] {RelativePath} ({Language}{(LanguageTags.Count > 0 ? "." : "")}{LanguageTagsAsString}{Extension})";
|
||||
var stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendFormat("[{0}] ", Id);
|
||||
stringBuilder.Append(RelativePath);
|
||||
|
||||
stringBuilder.Append(" (");
|
||||
stringBuilder.Append(Language);
|
||||
if (Title is not null)
|
||||
{
|
||||
stringBuilder.Append('.');
|
||||
stringBuilder.Append(Title);
|
||||
}
|
||||
|
||||
if (LanguageTags.Count > 0)
|
||||
{
|
||||
stringBuilder.Append('.');
|
||||
stringBuilder.Append(LanguageTagsAsString);
|
||||
}
|
||||
|
||||
stringBuilder.Append(Extension);
|
||||
stringBuilder.Append(')');
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user