1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-17 21:26:13 -04:00

Compare commits

...

45 Commits

Author SHA1 Message Date
Weblate
200396ef7a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-01-31 19:39:13 -08:00
Stevie Robinson
c5a724f14e New: Send 'On Manual Interaction Required' notifications in more cases
Closes #6448
2024-01-31 19:38:51 -08:00
Mark McDowall
42b11528b4 New: Improve multi-language negate Custom Format
Closes #6408
2024-01-31 19:37:44 -08:00
Alex Herbig
e2210228b3 New: Add RZeroX to release group parsing exceptions 2024-01-31 19:36:39 -08:00
Bogdan
ded7c3c6e2 Only bind shortcut for pending changes confirmation when it's shown 2024-01-31 19:36:21 -08:00
Stevie Robinson
e1c6722aad New: Ignore 'Other' subfolder when scanning disk
Closes #6437
2024-01-31 19:35:21 -08:00
Bogdan
e17655c26a Fixed: Notifications with only On Series Add enabled being labeled as disabled 2024-01-31 19:34:51 -08:00
Stevie Robinson
e66c628241 Update some translation keys 2024-01-31 19:34:17 -08:00
Mark McDowall
8f0514a91d Fixed: Grouped calendar events not correctly showing as downloading
Closes #6441
2024-01-31 19:33:46 -08:00
bakerboy448
d7aea82e45 Improve Release Grabbing & Failure Logging 2024-01-31 19:33:38 -08:00
Mark McDowall
19db75b36b Add max token length (including ellipsis) for some tokens
New: Accept ':##' on renaming tokens to allow specifying a maximum length for series, episode titles and release group
Closes #6416
2024-01-31 19:33:21 -08:00
Weblate
11a18b534a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Crocmou <slaanesh8854@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lars <lars.erik.heloe@gmail.com>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Stas Panasiuk <temnyip@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-01-31 19:33:13 -08:00
Sonarr
70807a9dcf Automated API Docs update
ignore-downstream
2024-01-26 22:09:26 -08:00
Weblate
350600607d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Alexander <a.burdun@gmail.com>
Co-authored-by: Crocmou <slaanesh8854@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: diaverso <alexito_perez.95@hotmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: zichichi <sollami@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-01-26 22:09:16 -08:00
Mark McDowall
e9f0c96249 Fixed: Specials not allowing multi-episode select in Manual Import
Closes #6429
2024-01-26 22:01:04 -08:00
ta264
d9acbf5682 Fixed: FolderWritable check for CIFS shares mounted in Unix
This reverts commit 8c892a732ed57af9bb1f39743e0c16361f41b50f.

(cherry picked from commit 96384521c59233dab5bd8289e7c84043f75b84a2)
2024-01-26 22:00:50 -08:00
Stevie Robinson
07cbd7c8d2 Fixed: Validating DownloadStation output path
Closes #6421
2024-01-27 00:59:43 -05:00
Mark McDowall
0ea189d03c Fixed: History retention for Newsbin 2024-01-26 21:56:13 -08:00
Bogdan
9e3f9f9618 Fixed: Testing for disabled Notifications 2024-01-26 21:56:05 -08:00
The Dark
68c326ae27 New: Import list clean library option
Closes #5201
2024-01-27 00:55:52 -05:00
Weblate
46367d2023 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-01-26 21:54:57 -08:00
Sonarr
b64c52a846 Automated API Docs update
ignore-downstream
2024-01-22 20:59:24 -08:00
Mark McDowall
345854d0fe New: Optionally remove from queue by changing category to 'Post-Import Category' when configured
Closes #6023
2024-01-22 23:56:35 -05:00
Bogdan
31baed4b2c Fixed: Sorting by name in Manage Indexer and Download Client modals 2024-01-22 20:56:01 -08:00
Bogdan
7d0d503a5e New: Display database migration version in Status 2024-01-22 20:55:53 -08:00
Stevie Robinson
9f50166fa6 Fixed: Regular Expression Custom Format translation 2024-01-22 23:55:33 -05:00
Bogdan
3c1ca6ea4e New: Expand seasons with all episodes having missing air dates 2024-01-22 20:54:35 -08:00
Mark McDowall
3cd4c67ba1 New: Add download client name to pending items waiting for a specific client
Closes #6274
2024-01-22 20:52:01 -08:00
Mark McDowall
fc3a2e9ab2 New: Added some extra pixels to grouped calendar events
Closes #6395
2024-01-22 20:51:53 -08:00
Mark McDowall
a71d40edba New: Add recycle bin path for deleted episodes to webhook/custom script
Closes #6114
2024-01-22 20:51:38 -08:00
Mark McDowall
9ba5850fca Fixed: Parsing Hungarian anime releases
Closes #6275
2024-01-22 20:51:27 -08:00
Mark McDowall
0d06418194 New: Add size to more history events
Closes #6234
2024-01-22 20:51:19 -08:00
Mark McDowall
f95dd00b51 Fixed: Migrating subtitle files with unexpectedly large number at end
Closes #6409
2024-01-22 20:50:43 -08:00
Bogdan
271266b10a Fix possible NullRef in Email Encryption migration 2024-01-22 20:50:34 -08:00
Mark McDowall
cab93249ec Fixed: Number only hashes getting substituted incorrectly 2024-01-20 16:44:12 -08:00
Mark McDowall
8921c5d7a0 Fixed: Subtitle title migration when original title is null 2024-01-20 16:43:53 -08:00
Weblate
dbbf1a7f58 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-01-20 16:43:40 -08:00
Jendrik Weise
69f99373e5 New: Parse subtitle titles
Closes #5955
2024-01-20 15:19:33 -08:00
Mark McDowall
7be5732a3a New: Option to disable Email encryption
Closes #6380
2024-01-20 15:18:26 -08:00
Mark McDowall
e66ba84fc0 New: Log warning if less than 1 GB free space during update
Closes #6385
2024-01-20 15:18:06 -08:00
Mark McDowall
c0b30a5028 Fixed: Series poster view on mobile devices
Closes #6387
2024-01-20 15:17:55 -08:00
Bogdan
3cf4d2907e Transpile logical assignment operators with babel 2024-01-20 15:17:42 -08:00
Weblate
ae96ebca57 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Bastián Quezada <baskezada@gmail.com>
Co-authored-by: Blair Noctis <fqmxz5hyfba7ft85@neon.casa>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Deleted User <noreply+2593@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Julian Baquero <julian-baquero@upc.edu.co>
Co-authored-by: Koch Norbert <kochnorbert@icloud.com>
Co-authored-by: MaddionMax <kovacs.tamas@ius.hu>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: brn <barantsenkul@gmail.com>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-01-20 15:17:33 -08:00
Mark McDowall
d336aaf3f0 Fixed: Don't clone indexer API Key
Closes #6265
2024-01-19 21:30:34 -08:00
bakerboy448
ec40bc6eea Improve Release Title Custom Format debugging
Towards #5598
2024-01-19 21:30:24 -08:00
168 changed files with 5475 additions and 918 deletions

View File

@@ -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 }],

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -43,6 +43,7 @@
.expandContainer,
.collapseContainer {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -224,16 +224,19 @@ class CalendarEventGroup extends Component {
</div>
{
showEpisodeInformation &&
showEpisodeInformation ?
<Link
className={styles.expandContainer}
component="div"
onPress={this.onExpandPress}
>
&nbsp;
<Icon
name={icons.EXPAND}
/>
</Link>
&nbsp;
</Link> :
null
}
</div>
);

View File

@@ -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));
});
}
);

View File

@@ -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,

View File

@@ -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,
}
);

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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')} />

View File

@@ -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}

View 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;

View File

@@ -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}

View File

@@ -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 = {

View File

@@ -88,7 +88,7 @@ function EditDelayProfileModalContent(props) {
{
!isFetching && !!error ?
<div>
{translate('AddQualityProfileError')}
{translate('AddDelayProfileError')}
</div> :
null
}

View File

@@ -94,7 +94,12 @@ export default {
items: [],
pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
name: function(item) {
return item.name.toLowerCase();
}
}
},
//

View 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)
}
};

View File

@@ -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;

View File

@@ -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',

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -0,0 +1,10 @@
export type ListSyncLevel =
| 'disabled'
| 'logOnly'
| 'keepAndUnmonitor'
| 'keepAndTag';
export default interface ImportListOptionsSettings {
listSyncLevel: ListSyncLevel;
listSyncTag: number;
}

View 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]>;
};

View File

@@ -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()

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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))));

View File

@@ -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());
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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,22 +137,281 @@ 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_not_clean_library_if_lists_have_not_removed_any_items()
{
_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);
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_search_if_series_title_and_no_series_id()
{
Subject.Execute(new ImportListSyncCommand());
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
WithList(1, true);
Subject.Execute(_commandAll);
Mocker.GetMock<ISearchForNewSeries>()
.Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Once());
@@ -105,8 +420,10 @@ namespace NzbDrone.Core.Test.ImportListTests
[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 +432,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 +444,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 +459,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 +473,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 +502,7 @@ namespace NzbDrone.Core.Test.ImportListTests
{
Mocker.GetMock<IFetchAndParseImportList>()
.Setup(v => v.Fetch())
.Returns(new List<ImportListItemInfo>());
.Returns(new ImportListFetchResult());
Subject.Execute(new ImportListSyncCommand());

View File

@@ -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(),
});

View File

@@ -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");
}
}
}

View File

@@ -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();

View File

@@ -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)
}
};

View File

@@ -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
{

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -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();
}
}
}

View File

@@ -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);

View File

@@ -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); }

View File

@@ -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; }

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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));

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Http;

View File

@@ -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}";
}

View File

@@ -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}";
}

View File

@@ -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)),

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);

View File

@@ -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,

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 = "DownloadClientTransmissionSettingsDirectoryHelpText")]
[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)]

View File

@@ -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));

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
};
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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();
}
}
}

View File

@@ -76,16 +76,20 @@ namespace NzbDrone.Core.Extras.Subtitles
foreach (var group in groupedExtraFilesForEpisodeFile)
{
var groupCount = group.Count();
var copy = 1;
var multipleCopies = group.Count() > 1;
var orderedGroup = group.OrderBy(s => -s.Copy).ToList();
var copy = group.First().Copy;
foreach (var subtitleFile in group)
foreach (var subtitleFile in orderedGroup)
{
var suffix = GetSuffix(subtitleFile.Language, copy, subtitleFile.LanguageTags, groupCount > 1);
if (multipleCopies && subtitleFile.Copy == 0)
{
subtitleFile.Copy = ++copy;
}
var suffix = GetSuffix(subtitleFile.Language, subtitleFile.Copy, subtitleFile.LanguageTags, multipleCopies, subtitleFile.Title);
movedFiles.AddIfNotNull(MoveFile(series, episodeFile, subtitleFile, suffix));
copy++;
}
}
}
@@ -229,11 +233,22 @@ namespace NzbDrone.Core.Extras.Subtitles
return importedFiles;
}
private string GetSuffix(Language language, int copy, List<string> languageTags, bool multipleCopies = false)
private string GetSuffix(Language language, int copy, List<string> languageTags, bool multipleCopies = false, string title = null)
{
var suffixBuilder = new StringBuilder();
if (multipleCopies)
if (title is not null)
{
suffixBuilder.Append('.');
suffixBuilder.Append(title);
if (multipleCopies)
{
suffixBuilder.Append(" - ");
suffixBuilder.Append(copy);
}
}
else if (multipleCopies)
{
suffixBuilder.Append('.');
suffixBuilder.Append(copy);

View File

@@ -219,6 +219,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name);
history.Data.Add("ReleaseGroup", message.EpisodeInfo.ReleaseGroup);
history.Data.Add("CustomFormatScore", message.EpisodeInfo.CustomFormatScore.ToString());
history.Data.Add("Size", message.EpisodeInfo.Size.ToString());
_historyRepository.Insert(history);
}
@@ -244,6 +245,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadClientName", message.TrackedDownload?.DownloadItem.DownloadClientInfo.Name);
history.Data.Add("Message", message.Message);
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseGroup);
history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString());
_historyRepository.Insert(history);
}
@@ -277,6 +279,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Reason", message.Reason.ToString());
history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup);
history.Data.Add("Size", message.EpisodeFile.Size.ToString());
_historyRepository.Insert(history);
}
@@ -307,6 +310,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Path", path);
history.Data.Add("RelativePath", relativePath);
history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup);
history.Data.Add("Size", message.EpisodeFile.Size.ToString());
_historyRepository.Insert(history);
}
@@ -334,6 +338,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadClientName", message.DownloadClientInfo.Name);
history.Data.Add("Message", message.Message);
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseGroup);
history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString());
historyToAdd.Add(history);
}

View File

@@ -5,7 +5,6 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.AniList
@@ -65,7 +64,7 @@ namespace NzbDrone.Core.ImportLists.AniList
return new { };
}
public override IList<ImportListItemInfo> Fetch()
public override ImportListFetchResult Fetch()
{
CheckToken();
return base.Fetch();

View File

@@ -44,10 +44,11 @@ namespace NzbDrone.Core.ImportLists.AniList.List
return new AniListParser(Settings);
}
protected override IList<ImportListItemInfo> FetchItems(Func<IImportListRequestGenerator, ImportListPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
protected override ImportListFetchResult FetchItems(Func<IImportListRequestGenerator, ImportListPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
{
var releases = new List<ImportListItemInfo>();
var url = string.Empty;
var anyFailure = true;
try
{
@@ -77,6 +78,7 @@ namespace NzbDrone.Core.ImportLists.AniList.List
while (hasNextPage);
_importListStatusService.RecordSuccess(Definition.Id);
anyFailure = false;
}
catch (WebException webException)
{
@@ -149,7 +151,7 @@ namespace NzbDrone.Core.ImportLists.AniList.List
_logger.Error(ex, "An error occurred while processing feed. {0}", url);
}
return CleanupListItems(releases);
return new ImportListFetchResult(CleanupListItems(releases), anyFailure);
}
}
}

View File

@@ -30,9 +30,10 @@ namespace NzbDrone.Core.ImportLists.Custom
_customProxy = customProxy;
}
public override IList<ImportListItemInfo> Fetch()
public override ImportListFetchResult Fetch()
{
var series = new List<ImportListItemInfo>();
var anyFailure = false;
try
{
@@ -50,12 +51,13 @@ namespace NzbDrone.Core.ImportLists.Custom
}
catch (Exception ex)
{
anyFailure = true;
_logger.Debug(ex, "Failed to fetch data for list {0} ({1})", Definition.Name, Name);
_importListStatusService.RecordFailure(Definition.Id);
}
return CleanupListItems(series);
return new ImportListFetchResult(CleanupListItems(series), anyFailure);
}
public override object RequestAction(string action, IDictionary<string, string> query)

View File

@@ -4,32 +4,34 @@ using System.Linq;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ImportLists.ImportListItems;
namespace NzbDrone.Core.ImportLists
{
public interface IFetchAndParseImportList
{
List<ImportListItemInfo> Fetch();
List<ImportListItemInfo> FetchSingleList(ImportListDefinition definition);
ImportListFetchResult Fetch();
ImportListFetchResult FetchSingleList(ImportListDefinition definition);
}
public class FetchAndParseImportListService : IFetchAndParseImportList
{
private readonly IImportListFactory _importListFactory;
private readonly IImportListStatusService _importListStatusService;
private readonly IImportListItemService _importListItemService;
private readonly Logger _logger;
public FetchAndParseImportListService(IImportListFactory importListFactory, IImportListStatusService importListStatusService, Logger logger)
public FetchAndParseImportListService(IImportListFactory importListFactory, IImportListStatusService importListStatusService, IImportListItemService importListItemService, Logger logger)
{
_importListFactory = importListFactory;
_importListStatusService = importListStatusService;
_importListItemService = importListItemService;
_logger = logger;
}
public List<ImportListItemInfo> Fetch()
public ImportListFetchResult Fetch()
{
var result = new List<ImportListItemInfo>();
var result = new ImportListFetchResult();
var importLists = _importListFactory.AutomaticAddEnabled();
@@ -47,7 +49,7 @@ namespace NzbDrone.Core.ImportLists
foreach (var importList in importLists)
{
var importListLocal = importList;
var importListStatus = _importListStatusService.GetLastSyncListInfo(importListLocal.Definition.Id);
var importListStatus = _importListStatusService.GetListStatus(importListLocal.Definition.Id).LastInfoSync;
if (importListStatus.HasValue)
{
@@ -64,16 +66,23 @@ namespace NzbDrone.Core.ImportLists
{
try
{
var importListReports = importListLocal.Fetch();
var fetchResult = importListLocal.Fetch();
var importListReports = fetchResult.Series;
lock (result)
{
_logger.Debug("Found {0} reports from {1} ({2})", importListReports.Count, importList.Name, importListLocal.Definition.Name);
result.AddRange(importListReports);
}
if (!fetchResult.AnyFailure)
{
importListReports.ForEach(s => s.ImportListId = importList.Definition.Id);
result.Series.AddRange(importListReports);
var removed = _importListItemService.SyncSeriesForList(importListReports, importList.Definition.Id);
_importListStatusService.UpdateListSyncStatus(importList.Definition.Id, removed > 0);
}
_importListStatusService.UpdateListSyncStatus(importList.Definition.Id);
result.AnyFailure |= fetchResult.AnyFailure;
}
}
catch (Exception e)
{
@@ -86,16 +95,16 @@ namespace NzbDrone.Core.ImportLists
Task.WaitAll(taskList.ToArray());
result = result.DistinctBy(r => new { r.TvdbId, r.ImdbId, r.Title }).ToList();
result.Series = result.Series.DistinctBy(r => new { r.TvdbId, r.ImdbId, r.Title }).ToList();
_logger.Debug("Found {0} total reports from {1} lists", result.Count, importLists.Count);
_logger.Debug("Found {0} total reports from {1} lists", result.Series.Count, importLists.Count);
return result;
}
public List<ImportListItemInfo> FetchSingleList(ImportListDefinition definition)
public ImportListFetchResult FetchSingleList(ImportListDefinition definition)
{
var result = new List<ImportListItemInfo>();
var result = new ImportListFetchResult();
var importList = _importListFactory.GetInstance(definition);
@@ -114,16 +123,25 @@ namespace NzbDrone.Core.ImportLists
{
try
{
var importListReports = importListLocal.Fetch();
var fetchResult = importListLocal.Fetch();
var importListReports = fetchResult.Series;
lock (result)
{
_logger.Debug("Found {0} reports from {1} ({2})", importListReports.Count, importList.Name, importListLocal.Definition.Name);
result.AddRange(importListReports);
if (!fetchResult.AnyFailure)
{
importListReports.ForEach(s => s.ImportListId = importList.Definition.Id);
result.Series.AddRange(importListReports);
var removed = _importListItemService.SyncSeriesForList(importListReports, importList.Definition.Id);
_importListStatusService.UpdateListSyncStatus(importList.Definition.Id, removed > 0);
}
result.AnyFailure |= fetchResult.AnyFailure;
}
_importListStatusService.UpdateListSyncStatus(importList.Definition.Id);
result.AnyFailure |= fetchResult.AnyFailure;
}
catch (Exception e)
{
@@ -135,8 +153,6 @@ namespace NzbDrone.Core.ImportLists
Task.WaitAll(taskList.ToArray());
result = result.DistinctBy(r => new { r.TvdbId, r.ImdbId, r.Title }).ToList();
return result;
}
}

View File

@@ -38,15 +38,16 @@ namespace NzbDrone.Core.ImportLists
_httpClient = httpClient;
}
public override IList<ImportListItemInfo> Fetch()
public override ImportListFetchResult Fetch()
{
return FetchItems(g => g.GetListItems(), true);
}
protected virtual IList<ImportListItemInfo> FetchItems(Func<IImportListRequestGenerator, ImportListPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
protected virtual ImportListFetchResult FetchItems(Func<IImportListRequestGenerator, ImportListPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
{
var releases = new List<ImportListItemInfo>();
var url = string.Empty;
var anyFailure = true;
try
{
@@ -92,6 +93,7 @@ namespace NzbDrone.Core.ImportLists
}
_importListStatusService.RecordSuccess(Definition.Id);
anyFailure = false;
}
catch (WebException webException)
{
@@ -163,7 +165,7 @@ namespace NzbDrone.Core.ImportLists
_logger.Error(ex, "An error occurred while processing feed. {0}", url);
}
return CleanupListItems(releases);
return new ImportListFetchResult(CleanupListItems(releases), anyFailure);
}
protected virtual bool IsValidItem(ImportListItemInfo listItem)

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.ImportLists
@@ -9,6 +7,6 @@ namespace NzbDrone.Core.ImportLists
{
ImportListType ListType { get; }
TimeSpan MinRefreshInterval { get; }
IList<ImportListItemInfo> Fetch();
ImportListFetchResult Fetch();
}
}

View File

@@ -11,6 +11,23 @@ using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.ImportLists
{
public class ImportListFetchResult
{
public ImportListFetchResult()
{
Series = new List<ImportListItemInfo>();
}
public ImportListFetchResult(IEnumerable<ImportListItemInfo> series, bool anyFailure)
{
Series = series.ToList();
AnyFailure = anyFailure;
}
public List<ImportListItemInfo> Series { get; set; }
public bool AnyFailure { get; set; }
}
public abstract class ImportListBase<TSettings> : IImportList
where TSettings : IImportListSettings, new()
{
@@ -63,7 +80,7 @@ namespace NzbDrone.Core.ImportLists
protected TSettings Settings => (TSettings)Definition.Settings;
public abstract IList<ImportListItemInfo> Fetch();
public abstract ImportListFetchResult Fetch();
protected virtual IList<ImportListItemInfo> CleanupListItems(IEnumerable<ImportListItemInfo> releases)
{

Some files were not shown because too many files have changed in this diff Show More