Compare commits

..

23 Commits

Author SHA1 Message Date
Qstick
57dcd861a9 Fixed: Validation for nested settings not running
Prevents #1243
2022-12-18 23:20:46 -06:00
Qstick
dfe132cda2 Fixed: Retain direct Indexer properties not affiliated with Prowlarr
Fixes #1165
2022-12-18 21:46:14 -06:00
Qstick
a635820b48 New: Sync Indexers button on index page
Fixes #92
2022-12-18 21:33:30 -06:00
Qstick
d959e81efb Modify Nab tests to pass for additional parameters
Fixes #1236
2022-12-18 21:19:03 -06:00
Qstick
ac89cd636f New: Separate setting for Pack Seed Time 2022-12-18 20:52:07 -06:00
Qstick
50616f5c9e Fixed: Don't mess with options we don't set on full sync 2022-12-18 20:52:07 -06:00
Qstick
3f9cb2c6ea Fixed: String compare in arr Indexer equality 2022-12-18 20:52:06 -06:00
Qstick
b5aa85a548 New: (Nebulance) Convert to API 2022-12-18 18:12:18 -06:00
Qstick
0fa5127c83 Cleanup dev logging in UI 2022-12-18 12:56:03 -06:00
Weblate
4d137886bc Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 89.6% (416 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (464 of 464 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: benniblot <ben2004engler@gmail.com>
Co-authored-by: mhng98 <mark.groenewegen@hotmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2022-12-18 10:41:16 -06:00
Qstick
9dde041c99 New: Search by description on add indexer modal
Fixes #1000
2022-12-18 00:13:44 -06:00
Qstick
a8234c9ce0 Fixed: Refresh applicable healthchecks on bulk deletes 2022-12-18 00:02:59 -06:00
Qstick
9227efdb65 New: (FileList) Freeleech Only option
Fixes #1147
2022-12-17 23:34:08 -06:00
Qstick
fa923e658f Fixed: (Nyaa) Torrent Age in UI incorrect
Fixes #144
2022-12-17 23:14:56 -06:00
bakerboy448
364a5564ae update-no-results-msg 2022-12-17 21:34:06 -06:00
Qstick
9efd0b391e fixup! 2022-12-17 21:31:37 -06:00
Qstick
320161e051 New: Smarter Newznab category mapping 2022-12-17 21:31:37 -06:00
Servarr
38ba810ae8 Automated API Docs update 2022-12-17 14:18:15 -06:00
Bakerboy448
4e3f460a24 Fixed: (Avistaz Family) Correct Age Parsing
Co-authored-by: Qstick <qstick@gmail.com>
2022-12-17 14:12:23 -06:00
Qstick
0d918a0aa9 New: Define multiple mapped categories for Download Clients
Fixes #170
2022-12-17 14:11:09 -06:00
Qstick
a110412665 Fixed: Stats failing of all indexer events are failures
Fixes #1231
2022-12-17 10:27:14 -06:00
Weblate
6c97f1b6ee Translated using Weblate (Italian)
Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 89.6% (416 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 89.6% (416 of 464 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Mipiaceanutella <remix-polity-0l@icloud.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Robin Flikkema <robin@robinflikkema.nl>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: kamperzoid <nick@kamper.be>
Co-authored-by: reloxx <reloxx@interia.pl>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translation: Servarr/Prowlarr
2022-12-11 12:47:35 -06:00
Qstick
470779ead2 Bump version to 0.4.11 2022-12-11 12:46:58 -06:00
121 changed files with 1539 additions and 405 deletions

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.10'
majorVersion: '0.4.11'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'

View File

@@ -16,6 +16,7 @@ import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
import InfoInput from './InfoInput';
import KeyValueListInput from './KeyValueListInput';
import NewznabCategorySelectInputConnector from './NewznabCategorySelectInputConnector';
import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput';
@@ -68,6 +69,9 @@ function getComponent(type) {
case inputTypes.PATH:
return PathInputConnector;
case inputTypes.CATEGORY_SELECT:
return NewznabCategorySelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector;

View File

@@ -31,7 +31,7 @@ function createMapStateToProps() {
});
return {
value,
value: value || [],
values
};
}

View File

@@ -8,6 +8,7 @@ export const DEVICE = 'device';
export const KEY_VALUE_LIST = 'keyValueList';
export const INFO = 'info';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const CATEGORY_SELECT = 'newznabCategorySelect';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
@@ -32,6 +33,7 @@ export const all = [
KEY_VALUE_LIST,
INFO,
MOVIE_MONITORED_SELECT,
CATEGORY_SELECT,
NUMBER,
OAUTH,
PASSWORD,

View File

@@ -123,7 +123,7 @@ class AddIndexerModalContent extends Component {
const filteredIndexers = indexers.filter((indexer) => {
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase())) {
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
return false;
}

View File

@@ -272,6 +272,7 @@ class IndexerIndex extends Component {
saveError,
isDeleting,
isTestingAll,
isSyncingIndexers,
deleteError,
onScroll,
onSortSelect,
@@ -309,6 +310,15 @@ class IndexerIndex extends Component {
onPress={this.onAddIndexerPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('SyncAppIndexers')}
iconName={icons.REFRESH}
isSpinning={isSyncingIndexers}
onPress={this.props.onAppIndexerSyncPress}
/>
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
@@ -493,10 +503,12 @@ IndexerIndex.propTypes = {
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
isTestingAll: PropTypes.bool.isRequired,
isSyncingIndexers: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onTestAllPress: PropTypes.func.isRequired,
onAppIndexerSyncPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};

View File

@@ -2,10 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import { testAllIndexers } from 'Store/Actions/indexerActions';
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
import IndexerIndex from './IndexerIndex';
@@ -13,13 +16,16 @@ import IndexerIndex from './IndexerIndex';
function createMapStateToProps() {
return createSelector(
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
createDimensionsSelector(),
(
indexers,
isSyncingIndexers,
dimensionsState
) => {
return {
...indexers,
isSyncingIndexers,
isSmallScreen: dimensionsState.isSmallScreen
};
}
@@ -46,6 +52,12 @@ function createMapDispatchToProps(dispatch, props) {
onTestAllPress() {
dispatch(testAllIndexers());
},
onAppIndexerSyncPress() {
dispatch(executeCommand({
name: commandNames.APP_INDEXER_SYNC
}));
}
};
}

View File

@@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AddCategoryModalContentConnector from './AddCategoryModalContentConnector';
function AddCategoryModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddCategoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddCategoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddCategoryModal;

View File

@@ -0,0 +1,50 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import AddCategoryModal from './AddCategoryModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.downloadClientCategories';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
}
};
}
class AddCategoryModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.onModalClose();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
...otherProps
} = this.props;
return (
<AddCategoryModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
AddCategoryModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(AddCategoryModalConnector);

View File

@@ -0,0 +1,5 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}

View File

@@ -0,0 +1,111 @@
import PropTypes from 'prop-types';
import React from 'react';
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 Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
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 } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './AddCategoryModalContent.css';
function AddCategoryModalContent(props) {
const {
advancedSettings,
item,
onInputChange,
onFieldChange,
onCancelPress,
onSavePress,
onDeleteSpecificationPress,
...otherProps
} = props;
const {
id,
clientCategory,
categories
} = item;
return (
<ModalContent onModalClose={onCancelPress}>
<ModalHeader>
{`${id ? 'Edit' : 'Add'} Category`}
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>
{translate('DownloadClientCategory')}
</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="clientCategory"
{...clientCategory}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MappedCategories')}
</FormLabel>
<FormInputGroup
type={inputTypes.CATEGORY_SELECT}
name="categories"
{...categories}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteSpecificationPress}
>
{translate('Delete')}
</Button>
}
<Button
onPress={onCancelPress}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={false}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
AddCategoryModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onCancelPress: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onDeleteSpecificationPress: PropTypes.func
};
export default AddCategoryModalContent;

View File

@@ -0,0 +1,78 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearDownloadClientCategoryPending, saveDownloadClientCategory, setDownloadClientCategoryFieldValue, setDownloadClientCategoryValue } from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import AddCategoryModalContent from './AddCategoryModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('downloadClientCategories'),
(advancedSettings, specification) => {
return {
advancedSettings,
...specification
};
}
);
}
const mapDispatchToProps = {
setDownloadClientCategoryValue,
setDownloadClientCategoryFieldValue,
saveDownloadClientCategory,
clearDownloadClientCategoryPending
};
class AddCategoryModalContentConnector extends Component {
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setDownloadClientCategoryValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setDownloadClientCategoryFieldValue({ name, value });
};
onCancelPress = () => {
this.props.clearDownloadClientCategoryPending();
this.props.onModalClose();
};
onSavePress = () => {
this.props.saveDownloadClientCategory({ id: this.props.id });
this.props.onModalClose();
};
//
// Render
render() {
return (
<AddCategoryModalContent
{...this.props}
onCancelPress={this.onCancelPress}
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
AddCategoryModalContentConnector.propTypes = {
id: PropTypes.number,
item: PropTypes.object.isRequired,
setDownloadClientCategoryValue: PropTypes.func.isRequired,
setDownloadClientCategoryFieldValue: PropTypes.func.isRequired,
clearDownloadClientCategoryPending: PropTypes.func.isRequired,
saveDownloadClientCategory: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddCategoryModalContentConnector);

View File

@@ -0,0 +1,32 @@
.customFormat {
composes: card from '~Components/Card.css';
width: 300px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
margin-bottom: 5px;
font-weight: 300;
font-size: 20px;
}
.labels {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
pointer-events: all;
}
.tooltipLabel {
composes: label from '~Components/Label.css';
margin: 0;
border: none;
}

View File

@@ -0,0 +1,111 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddCategoryModalConnector from './AddCategoryModalConnector';
import styles from './Category.css';
class Category extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditSpecificationModalOpen: false,
isDeleteSpecificationModalOpen: false
};
}
//
// Listeners
onEditSpecificationPress = () => {
this.setState({ isEditSpecificationModalOpen: true });
};
onEditSpecificationModalClose = () => {
this.setState({ isEditSpecificationModalOpen: false });
};
onDeleteSpecificationPress = () => {
this.setState({
isEditSpecificationModalOpen: false,
isDeleteSpecificationModalOpen: true
});
};
onDeleteSpecificationModalClose = () => {
this.setState({ isDeleteSpecificationModalOpen: false });
};
onConfirmDeleteSpecification = () => {
this.props.onConfirmDeleteSpecification(this.props.id);
};
//
// Lifecycle
render() {
const {
id,
clientCategory,
categories
} = this.props;
return (
<Card
className={styles.customFormat}
overlayContent={true}
onPress={this.onEditSpecificationPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>
{clientCategory}
</div>
</div>
<Label kind={kinds.PRIMARY}>
{`${categories.length} ${categories.length > 1 ? translate('Categories') : translate('Category')}`}
</Label>
<AddCategoryModalConnector
id={id}
isOpen={this.state.isEditSpecificationModalOpen}
onModalClose={this.onEditSpecificationModalClose}
onDeleteSpecificationPress={this.onDeleteSpecificationPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER}
title={translate('DeleteClientCategory')}
message={
<div>
<div>
{translate('AreYouSureYouWantToDeleteCategory', [name])}
</div>
</div>
}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose}
/>
</Card>
);
}
}
Category.propTypes = {
id: PropTypes.number.isRequired,
categories: PropTypes.arrayOf(PropTypes.number).isRequired,
clientCategory: PropTypes.string.isRequired,
onConfirmDeleteSpecification: PropTypes.func.isRequired
};
export default Category;

View File

@@ -1,11 +1,14 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Card from 'Components/Card';
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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -13,12 +16,33 @@ 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 } from 'Helpers/Props';
import { icons, inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddCategoryModalConnector from './Categories/AddCategoryModalConnector';
import Category from './Categories/Category';
import styles from './EditDownloadClientModalContent.css';
class EditDownloadClientModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddCategoryModalOpen: false
};
}
onAddCategoryPress = () => {
this.setState({ isAddCategoryModalOpen: true });
};
onAddCategoryModalClose = () => {
this.setState({ isAddCategoryModalOpen: false });
};
//
// Render
@@ -27,6 +51,7 @@ class EditDownloadClientModalContent extends Component {
advancedSettings,
isFetching,
error,
categories,
isSaving,
isTesting,
saveError,
@@ -37,15 +62,21 @@ class EditDownloadClientModalContent extends Component {
onSavePress,
onTestPress,
onDeleteDownloadClientPress,
onConfirmDeleteCategory,
...otherProps
} = this.props;
const {
isAddCategoryModalOpen
} = this.state;
const {
id,
implementationName,
name,
enable,
priority,
supportsCategories,
fields,
message
} = item;
@@ -136,6 +167,43 @@ class EditDownloadClientModalContent extends Component {
/>
</FormGroup>
{
supportsCategories.value ?
<FieldSet legend={translate('MappedCategories')}>
<div className={styles.customFormats}>
{
categories.map((tag) => {
return (
<Category
key={tag.id}
{...tag}
onConfirmDeleteSpecification={onConfirmDeleteCategory}
/>
);
})
}
<Card
className={styles.addCategory}
onPress={this.onAddCategoryPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={25}
/>
</div>
</Card>
</div>
</FieldSet> :
null
}
<AddCategoryModalConnector
isOpen={isAddCategoryModalOpen}
onModalClose={this.onAddCategoryModalClose}
/>
</Form>
}
</ModalBody>
@@ -185,13 +253,15 @@ EditDownloadClientModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isTesting: PropTypes.bool.isRequired,
categories: PropTypes.arrayOf(PropTypes.object),
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onDeleteDownloadClientPress: PropTypes.func
onDeleteDownloadClientPress: PropTypes.func,
onConfirmDeleteCategory: PropTypes.func.isRequired
};
export default EditDownloadClientModalContent;

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
import { deleteDownloadClientCategory, fetchDownloadClientCategories, saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
@@ -10,10 +10,12 @@ function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('downloadClients'),
(advancedSettings, downloadClient) => {
(state) => state.settings.downloadClientCategories,
(advancedSettings, downloadClient, categories) => {
return {
advancedSettings,
...downloadClient
...downloadClient,
categories: categories.items
};
}
);
@@ -23,7 +25,9 @@ const mapDispatchToProps = {
setDownloadClientValue,
setDownloadClientFieldValue,
saveDownloadClient,
testDownloadClient
testDownloadClient,
fetchDownloadClientCategories,
deleteDownloadClientCategory
};
class EditDownloadClientModalContentConnector extends Component {
@@ -31,6 +35,14 @@ class EditDownloadClientModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
id,
tagsFromId
} = this.props;
this.props.fetchDownloadClientCategories({ id: tagsFromId || id });
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
@@ -56,6 +68,10 @@ class EditDownloadClientModalContentConnector extends Component {
this.props.testDownloadClient({ id: this.props.id });
};
onConfirmDeleteCategory = (id) => {
this.props.deleteDownloadClientCategory({ id });
};
//
// Render
@@ -67,6 +83,7 @@ class EditDownloadClientModalContentConnector extends Component {
onTestPress={this.onTestPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
onConfirmDeleteCategory={this.onConfirmDeleteCategory}
/>
);
}
@@ -74,10 +91,13 @@ class EditDownloadClientModalContentConnector extends Component {
EditDownloadClientModalContentConnector.propTypes = {
id: PropTypes.number,
tagsFromId: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
fetchDownloadClientCategories: PropTypes.func.isRequired,
deleteDownloadClientCategory: PropTypes.func.isRequired,
setDownloadClientValue: PropTypes.func.isRequired,
setDownloadClientFieldValue: PropTypes.func.isRequired,
saveDownloadClient: PropTypes.func.isRequired,

View File

@@ -14,8 +14,6 @@ function createLanguagesSelector() {
return createSelector(
(state) => state.localization,
(localization) => {
console.log(localization);
const items = localization.items;
if (!items) {

View File

@@ -0,0 +1,169 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import getNextId from 'Utilities/State/getNextId';
import getProviderState from 'Utilities/State/getProviderState';
import getSectionState from 'Utilities/State/getSectionState';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import { removeItem, set, update, updateItem } from '../baseActions';
//
// Variables
const section = 'settings.downloadClientCategories';
//
// Actions Types
export const FETCH_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/fetchDownloadClientCategories';
export const FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/fetchDownloadClientCategorySchema';
export const SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/selectDownloadClientCategorySchema';
export const SET_DOWNLOAD_CLIENT_CATEGORY_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryValue';
export const SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryFieldValue';
export const SAVE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/saveDownloadClientCategory';
export const DELETE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteDownloadClientCategory';
export const DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteAllDownloadClientCategory';
export const CLEAR_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/clearDownloadClientCategories';
export const CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING = 'settings/downloadClientCategories/clearDownloadClientCategoryPending';
//
// Action Creators
export const fetchDownloadClientCategories = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORIES);
export const fetchDownloadClientCategorySchema = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
export const selectDownloadClientCategorySchema = createAction(SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
export const saveDownloadClientCategory = createThunk(SAVE_DOWNLOAD_CLIENT_CATEGORY);
export const deleteDownloadClientCategory = createThunk(DELETE_DOWNLOAD_CLIENT_CATEGORY);
export const deleteAllDownloadClientCategory = createThunk(DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY);
export const setDownloadClientCategoryValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_VALUE, (payload) => {
return {
section,
...payload
};
});
export const setDownloadClientCategoryFieldValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE, (payload) => {
return {
section,
...payload
};
});
export const clearDownloadClientCategory = createAction(CLEAR_DOWNLOAD_CLIENT_CATEGORIES);
export const clearDownloadClientCategoryPending = createThunk(CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING);
//
// Details
export default {
//
// State
defaultState: {
isPopulated: false,
error: null,
isSchemaFetching: false,
isSchemaPopulated: false,
schemaError: null,
schema: [],
selectedSchema: {},
isSaving: false,
saveError: null,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_DOWNLOAD_CLIENT_CATEGORIES]: (getState, payload, dispatch) => {
let tags = [];
if (payload.id) {
const cfState = getSectionState(getState(), 'settings.downloadClients', true);
const cf = cfState.items[cfState.itemMap[payload.id]];
tags = cf.categories.map((tag, i) => {
return {
id: i + 1,
...tag
};
});
}
dispatch(batchActions([
update({ section, data: tags }),
set({
section,
isPopulated: true
})
]));
},
[SAVE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
const {
id,
...otherPayload
} = payload;
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
// we have to set id since not actually posting to server yet
if (!saveData.id) {
saveData.id = getNextId(getState().settings.downloadClientCategories.items);
}
dispatch(batchActions([
updateItem({ section, ...saveData }),
set({
section,
pendingChanges: {}
})
]));
},
[DELETE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
const id = payload.id;
return dispatch(removeItem({ section, id }));
},
[DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
return dispatch(set({
section,
items: []
}));
},
[CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING]: (getState, payload, dispatch) => {
return dispatch(set({
section,
pendingChanges: {}
}));
}
},
//
// Reducers
reducers: {
[SET_DOWNLOAD_CLIENT_CATEGORY_VALUE]: createSetSettingValueReducer(section),
[SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
[SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
return selectedSchema;
});
},
[CLEAR_DOWNLOAD_CLIENT_CATEGORIES]: createClearReducer(section, {
isPopulated: false,
error: null,
items: []
})
}
};

View File

@@ -9,6 +9,7 @@ import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import { set } from '../baseActions';
//
// Variables
@@ -90,10 +91,34 @@ export default {
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
[SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler(section, '/downloadclient'),
[SAVE_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
// move the format tags in as a pending change
const state = getState();
const pendingChanges = state.settings.downloadClients.pendingChanges;
pendingChanges.categories = state.settings.downloadClientCategories.items;
dispatch(set({
section,
pendingChanges
}));
createSaveProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
},
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
[TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'),
[TEST_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
const state = getState();
const pendingChanges = state.settings.downloadClients.pendingChanges;
pendingChanges.categories = state.settings.downloadClientCategories.items;
dispatch(set({
section,
pendingChanges
}));
createTestProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
},
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
},

View File

@@ -310,8 +310,6 @@ export const actionHandlers = handleThunks({
isGrabbing: true
}));
console.log(payload);
const promise = createAjaxRequest({
url: '/search/bulk',
method: 'POST',

View File

@@ -4,6 +4,7 @@ import createHandleActions from './Creators/createHandleActions';
import applications from './Settings/applications';
import appProfiles from './Settings/appProfiles';
import development from './Settings/development';
import downloadClientCategories from './Settings/downloadClientCategories';
import downloadClients from './Settings/downloadClients';
import general from './Settings/general';
import indexerCategories from './Settings/indexerCategories';
@@ -11,6 +12,7 @@ import indexerProxies from './Settings/indexerProxies';
import notifications from './Settings/notifications';
import ui from './Settings/ui';
export * from './Settings/downloadClientCategories';
export * from './Settings/downloadClients';
export * from './Settings/general';
export * from './Settings/indexerCategories';
@@ -32,6 +34,7 @@ export const section = 'settings';
export const defaultState = {
advancedSettings: false,
downloadClientCategories: downloadClientCategories.defaultState,
downloadClients: downloadClients.defaultState,
general: general.defaultState,
indexerCategories: indexerCategories.defaultState,
@@ -61,6 +64,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
// Action Handlers
export const actionHandlers = handleThunks({
...downloadClientCategories.actionHandlers,
...downloadClients.actionHandlers,
...general.actionHandlers,
...indexerCategories.actionHandlers,
@@ -81,6 +85,7 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
},
...downloadClientCategories.reducers,
...downloadClients.reducers,
...general.reducers,
...indexerCategories.reducers,

View File

@@ -18,6 +18,7 @@
<subcat id="5030" name="SD"/>
<subcat id="5060" name="Sport"/>
<subcat id="5010" name="WEB-DL"/>
<subcat id="5999" name="Other"/>
</category>
<category id="7000" name="Other">
<subcat id="7010" name="Misc"/>

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.History;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerStats;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerStatsTests
{
public class IndexerStatisticsServiceFixture : CoreTest<IndexerStatisticsService>
{
private IndexerDefinition _indexer;
[SetUp]
public void Setup()
{
_indexer = Builder<IndexerDefinition>.CreateNew().With(x => x.Id = 5).Build();
Mocker.GetMock<IIndexerFactory>()
.Setup(o => o.All())
.Returns(new List<IndexerDefinition> { _indexer });
}
[Test]
public void should_pull_stats_if_all_events_are_failures()
{
var history = new List<History.History>
{
new History.History
{
Date = DateTime.UtcNow.AddHours(-1),
EventType = HistoryEventType.IndexerRss,
Successful = false,
Id = 8,
IndexerId = 5,
Data = new Dictionary<string, string> { { "source", "prowlarr" } }
}
};
Mocker.GetMock<IHistoryService>()
.Setup(o => o.Between(It.IsAny<DateTime>(), It.IsAny<DateTime>()))
.Returns<DateTime, DateTime>((s, f) => history);
var statistics = Subject.IndexerStatistics(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow);
statistics.IndexerStatistics.Count.Should().Be(1);
statistics.IndexerStatistics.First().AverageResponseTime.Should().Be(0);
}
}
}

View File

@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 23:26:21"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-15 04:26:21"));
torrentInfo.Size.Should().Be(935127615);
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 11:04:50"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 16:04:50"));
torrentInfo.Size.Should().Be(7085405541);
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 00:24:49"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
torrentInfo.Size.Should().Be(69914591044);
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Xml;
@@ -70,6 +71,32 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
caps.LimitsMax.Value.Should().Be(60);
}
[Test]
public void should_map_different_categories()
{
GivenCapsResponse(_caps);
var caps = Subject.GetCapabilities(_settings, _definition);
var bookCats = caps.Categories.MapTorznabCapsToTrackers(new int[] { NewznabStandardCategory.Books.Id });
bookCats.Count.Should().Be(2);
bookCats.Should().Contain("8000");
}
[Test]
public void should_map_by_name_when_available()
{
GivenCapsResponse(_caps);
var caps = Subject.GetCapabilities(_settings, _definition);
var bookCats = caps.Categories.MapTrackerCatToNewznab("5999");
bookCats.Count.Should().Be(2);
bookCats.First().Id.Should().Be(5050);
}
[Test]
public void should_use_default_pagesize_if_missing()
{

View File

@@ -126,6 +126,12 @@ namespace NzbDrone.Core.Applications.Lidarr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
// Retain user fields not-affiliated with Prowlarr
lidarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !lidarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Retain user settings not-affiliated with Prowlarr
lidarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
// Update the indexer if it still has categories that match
_lidarrV1Proxy.UpdateIndexer(lidarrIndexer, Settings);
}
@@ -159,6 +165,7 @@ namespace NzbDrone.Core.Applications.Lidarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +182,11 @@ namespace NzbDrone.Core.Applications.Lidarr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<LidarrField>()
};
lidarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -191,7 +200,7 @@ namespace NzbDrone.Core.Applications.Lidarr
if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
{
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
}
}

View File

@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public HashSet<int> Tags { get; set; }
public List<LidarrField> Fields { get; set; }
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Lidarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -126,6 +126,12 @@ namespace NzbDrone.Core.Applications.Radarr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
// Retain user fields not-affiliated with Prowlarr
radarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !radarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Retain user settings not-affiliated with Prowlarr
radarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
// Update the indexer if it still has categories that match
_radarrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
}
@@ -159,6 +165,7 @@ namespace NzbDrone.Core.Applications.Radarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +182,11 @@ namespace NzbDrone.Core.Applications.Radarr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<RadarrField>()
};
radarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;

View File

@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Radarr
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public HashSet<int> Tags { get; set; }
public List<RadarrField> Fields { get; set; }
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Radarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -126,6 +126,8 @@ namespace NzbDrone.Core.Applications.Readarr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
readarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !readarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Update the indexer if it still has categories that match
_readarrV1Proxy.UpdateIndexer(readarrIndexer, Settings);
}
@@ -159,6 +161,7 @@ namespace NzbDrone.Core.Applications.Readarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +178,11 @@ namespace NzbDrone.Core.Applications.Readarr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<ReadarrField>()
};
readarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -191,7 +196,7 @@ namespace NzbDrone.Core.Applications.Readarr
if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
{
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
}
}

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Readarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -126,6 +126,13 @@ namespace NzbDrone.Core.Applications.Sonarr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
{
// Retain user fields not-affiliated with Prowlarr
sonarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !sonarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Retain user settings not-affiliated with Prowlarr
sonarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
sonarrIndexer.SeasonSearchMaximumSingleEpisodeAge = remoteIndexer.SeasonSearchMaximumSingleEpisodeAge;
// Update the indexer if it still has categories that match
_sonarrV3Proxy.UpdateIndexer(sonarrIndexer, Settings);
}
@@ -159,6 +166,7 @@ namespace NzbDrone.Core.Applications.Sonarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +183,11 @@ namespace NzbDrone.Core.Applications.Sonarr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<SonarrField>()
};
sonarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -189,7 +199,7 @@ namespace NzbDrone.Core.Applications.Sonarr
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
}
return sonarrIndexer;

View File

@@ -17,6 +17,8 @@ namespace NzbDrone.Core.Applications.Sonarr
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public int? SeasonSearchMaximumSingleEpisodeAge { get; set; }
public HashSet<int> Tags { get; set; }
public List<SonarrField> Fields { get; set; }
@@ -34,7 +36,7 @@ namespace NzbDrone.Core.Applications.Sonarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -126,6 +126,8 @@ namespace NzbDrone.Core.Applications.Whisparr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
whisparrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !whisparrIndexer.Fields.Any(s => s.Name == f.Name)));
// Update the indexer if it still has categories that match
_whisparrV3Proxy.UpdateIndexer(whisparrIndexer, Settings);
}
@@ -159,6 +161,7 @@ namespace NzbDrone.Core.Applications.Whisparr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +178,11 @@ namespace NzbDrone.Core.Applications.Whisparr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<WhisparrField>()
};
whisparrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Whisparr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(023)]
public class download_client_categories : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("DownloadClients")
.AddColumn("Categories").AsString().WithDefaultValue("[]");
}
}
}

View File

@@ -60,7 +60,8 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<DownloadClientDefinition>("DownloadClients").RegisterModel()
.Ignore(x => x.ImplementationName)
.Ignore(i => i.Protocol)
.Ignore(d => d.SupportsCategories)
.Ignore(d => d.Protocol)
.Ignore(d => d.Tags);
Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel()
@@ -115,6 +116,7 @@ namespace NzbDrone.Core.Datastore
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<DownloadClientCategory>>());
SqlMapper.AddTypeHandler(new OsPathConverter());
SqlMapper.RemoveTypeMap(typeof(Guid));
SqlMapper.RemoveTypeMap(typeof(Guid?));

View File

@@ -18,6 +18,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2
public override string Name => "Aria2";
public override bool SupportsCategories => false;
public Aria2(IAria2Proxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,

View File

@@ -74,6 +74,8 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
public override string Name => "Torrent Blackhole";
public override bool SupportsCategories => false;
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestFolder(Settings.TorrentFolder, "TorrentFolder"));

View File

@@ -45,6 +45,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
}
public override string Name => "Usenet Blackhole";
public override bool SupportsCategories => false;
protected override void Test(List<ValidationFailure> failures)
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
@@ -38,9 +39,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge
}
// _proxy.SetTorrentSeedingConfiguration(actualHash, remoteMovie.SeedConfiguration, Settings);
if (Settings.Category.IsNotNullOrWhiteSpace())
var category = GetCategoryForRelease(release) ?? Settings.Category;
if (category.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(actualHash, Settings.Category, Settings);
_proxy.SetTorrentLabel(actualHash, category, Settings);
}
if (Settings.Priority == (int)DelugePriority.First)
@@ -61,9 +63,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge
}
// _proxy.SetTorrentSeedingConfiguration(actualHash, release.SeedConfiguration, Settings);
if (Settings.Category.IsNotNullOrWhiteSpace())
var category = GetCategoryForRelease(release) ?? Settings.Category;
if (category.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(actualHash, Settings.Category, Settings);
_proxy.SetTorrentLabel(actualHash, category, Settings);
}
if (Settings.Priority == (int)DelugePriority.First)
@@ -75,6 +78,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
}
public override string Name => "Deluge";
public override bool SupportsCategories => true;
protected override void Test(List<ValidationFailure> failures)
{
@@ -139,7 +143,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
private ValidationFailure TestCategory()
{
if (Settings.Category.IsNullOrWhiteSpace())
if (Categories.Count == 0)
{
return null;
}
@@ -156,23 +160,42 @@ namespace NzbDrone.Core.Download.Clients.Deluge
var labels = _proxy.GetAvailableLabels(Settings);
if (Settings.Category.IsNotNullOrWhiteSpace() && !labels.Contains(Settings.Category))
{
_proxy.AddLabel(Settings.Category, Settings);
labels = _proxy.GetAvailableLabels(Settings);
var categories = Categories.Select(c => c.ClientCategory).ToList();
categories.Add(Settings.Category);
if (!labels.Contains(Settings.Category))
foreach (var category in categories)
{
if (category.IsNotNullOrWhiteSpace() && !labels.Contains(category))
{
return new NzbDroneValidationFailure("Category", "Configuration of label failed")
_proxy.AddLabel(category, Settings);
labels = _proxy.GetAvailableLabels(Settings);
if (!labels.Contains(category))
{
DetailedDescription = "Prowlarr was unable to add the label to Deluge."
};
return new NzbDroneValidationFailure("Category", "Configuration of label failed")
{
DetailedDescription = "Prowlarr was unable to add the label to Deluge."
};
}
}
}
return null;
}
protected override void ValidateCategories(List<ValidationFailure> failures)
{
base.ValidateCategories(failures);
foreach (var label in Categories)
{
if (!Regex.IsMatch(label.ClientCategory, "^[-a-z0-9]*$"))
{
failures.AddIfNotNull(new ValidationFailure(string.Empty, "Mapped Categories allowed characters a-z, 0-9 and -"));
}
}
}
private ValidationFailure TestGetTorrents()
{
try

View File

@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
string[] GetAvailablePlugins(DelugeSettings settings);
string[] GetEnabledPlugins(DelugeSettings settings);
string[] GetAvailableLabels(DelugeSettings settings);
DelugeLabel GetLabelOptions(DelugeSettings settings);
DelugeLabel GetLabelOptions(DelugeSettings settings, string label);
void SetTorrentLabel(string hash, string label, DelugeSettings settings);
void SetTorrentConfiguration(string hash, string key, object value, DelugeSettings settings);
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, DelugeSettings settings);
@@ -158,9 +158,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge
return response;
}
public DelugeLabel GetLabelOptions(DelugeSettings settings)
public DelugeLabel GetLabelOptions(DelugeSettings settings, string label)
{
var response = ProcessRequest<DelugeLabel>(settings, "label.get_options", settings.Category);
var response = ProcessRequest<DelugeLabel>(settings, "label.get_options", label);
return response;
}

View File

@@ -43,7 +43,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(5, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback Category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
public string Category { get; set; }
[FieldDefinition(6, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing items")]

View File

@@ -18,9 +18,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
.WithMessage("Cannot start with /");
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
RuleFor(c => c.Category).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
RuleFor(c => c.TvCategory).Empty()
RuleFor(c => c.Category).Empty()
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
.WithMessage("Cannot use Category and Directory");
}
@@ -45,8 +45,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
public string TvCategory { get; set; }
[FieldDefinition(5, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
public string Category { get; set; }
[FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")]
public string TvDirectory { get; set; }

View File

@@ -44,6 +44,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
}
public override string Name => "Download Station";
public override bool SupportsCategories => false;
public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning);
@@ -198,7 +199,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
if (downloadDir != null)
{
var sharedFolder = downloadDir.Split('\\', '/')[0];
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.Category);
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
@@ -311,11 +312,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
{
return Settings.TvDirectory.TrimStart('/');
}
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
else if (Settings.Category.IsNotNullOrWhiteSpace())
{
var destDir = GetDefaultDir();
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
return $"{destDir.TrimEnd('/')}/{Settings.Category}";
}
return null;

View File

@@ -42,6 +42,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
}
public override string Name => "Download Station";
public override bool SupportsCategories => false;
public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning);
@@ -101,7 +102,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
if (downloadDir != null)
{
var sharedFolder = downloadDir.Split('\\', '/')[0];
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.Category);
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
@@ -272,11 +273,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
{
return Settings.TvDirectory.TrimStart('/');
}
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
else if (Settings.Category.IsNotNullOrWhiteSpace())
{
var destDir = GetDefaultDir();
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
return $"{destDir.TrimEnd('/')}/{Settings.Category}";
}
return null;

View File

@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
_proxy = proxy;
}
private static IEnumerable<string> HandleTags(ReleaseInfo release, FloodSettings settings)
private static IEnumerable<string> HandleTags(ReleaseInfo release, FloodSettings settings, string mappedCategory)
{
var result = new HashSet<string>();
@@ -36,6 +36,11 @@ namespace NzbDrone.Core.Download.Clients.Flood
result.UnionWith(settings.Tags);
}
if (mappedCategory != null)
{
result.Add(mappedCategory);
}
if (settings.AdditionalTags.Any())
{
foreach (var additionalTag in settings.AdditionalTags)
@@ -55,18 +60,19 @@ namespace NzbDrone.Core.Download.Clients.Flood
}
public override string Name => "Flood";
public override bool SupportsCategories => true;
public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to remove torrents that have finished seeding when using Flood", ProviderMessageType.Warning);
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
{
_proxy.AddTorrentByFile(Convert.ToBase64String(fileContent), HandleTags(release, Settings), Settings);
_proxy.AddTorrentByFile(Convert.ToBase64String(fileContent), HandleTags(release, Settings, GetCategoryForRelease(release)), Settings);
return hash;
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
{
_proxy.AddTorrentByUrl(magnetLink, HandleTags(release, Settings), Settings);
_proxy.AddTorrentByUrl(magnetLink, HandleTags(release, Settings, GetCategoryForRelease(release)), Settings);
return hash;
}

View File

@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
}
public override string Name => "Hadouken";
public override bool SupportsCategories => true;
protected override void Test(List<ValidationFailure> failures)
{
@@ -41,14 +42,14 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
{
_proxy.AddTorrentUri(Settings, magnetLink);
_proxy.AddTorrentUri(Settings, magnetLink, GetCategoryForRelease(release) ?? Settings.Category);
return hash.ToUpper();
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
{
return _proxy.AddTorrentFile(Settings, fileContent).ToUpper();
return _proxy.AddTorrentFile(Settings, fileContent, GetCategoryForRelease(release) ?? Settings.Category).ToUpper();
}
private ValidationFailure TestConnection()

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Net;
using NLog;
@@ -13,8 +13,8 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
HadoukenSystemInfo GetSystemInfo(HadoukenSettings settings);
HadoukenTorrent[] GetTorrents(HadoukenSettings settings);
IReadOnlyDictionary<string, object> GetConfig(HadoukenSettings settings);
string AddTorrentFile(HadoukenSettings settings, byte[] fileContent);
void AddTorrentUri(HadoukenSettings settings, string torrentUrl);
string AddTorrentFile(HadoukenSettings settings, byte[] fileContent, string label);
void AddTorrentUri(HadoukenSettings settings, string torrentUrl, string label);
void RemoveTorrent(HadoukenSettings settings, string downloadId);
void RemoveTorrentAndData(HadoukenSettings settings, string downloadId);
}
@@ -47,14 +47,14 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
return ProcessRequest<IReadOnlyDictionary<string, object>>(settings, "webui.getSettings");
}
public string AddTorrentFile(HadoukenSettings settings, byte[] fileContent)
public string AddTorrentFile(HadoukenSettings settings, byte[] fileContent, string label)
{
return ProcessRequest<string>(settings, "webui.addTorrent", "file", Convert.ToBase64String(fileContent), new { label = settings.Category });
return ProcessRequest<string>(settings, "webui.addTorrent", "file", Convert.ToBase64String(fileContent), new { label });
}
public void AddTorrentUri(HadoukenSettings settings, string torrentUrl)
public void AddTorrentUri(HadoukenSettings settings, string torrentUrl, string label)
{
ProcessRequest<string>(settings, "webui.addTorrent", "url", torrentUrl, new { label = settings.Category });
ProcessRequest<string>(settings, "webui.addTorrent", "url", torrentUrl, new { label });
}
public void RemoveTorrent(HadoukenSettings settings, string downloadId)

View File

@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox)]
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release.")]
public string Category { get; set; }
public NzbDroneValidationResult Validate()

View File

@@ -29,8 +29,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContents)
{
var priority = Settings.Priority;
var category = GetCategoryForRelease(release) ?? Settings.Category;
var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings);
var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings, category);
if (response == null)
{
@@ -41,6 +42,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
}
public override string Name => "NZBVortex";
public override bool SupportsCategories => true;
protected List<NzbVortexGroup> GetGroups()
{
@@ -111,19 +113,27 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
private ValidationFailure TestCategory()
{
var group = GetGroups().FirstOrDefault(c => c.GroupName == Settings.Category);
var groups = GetGroups();
if (group == null)
foreach (var category in Categories)
{
if (Settings.Category.IsNotNullOrWhiteSpace())
if (!category.ClientCategory.IsNullOrWhiteSpace() && !groups.Any(v => v.GroupName == category.ClientCategory))
{
return new NzbDroneValidationFailure("Category", "Group does not exist")
return new NzbDroneValidationFailure(string.Empty, "Group does not exist")
{
DetailedDescription = "The Group you entered doesn't exist in NzbVortex. Go to NzbVortex to create it."
DetailedDescription = "A mapped category you entered doesn't exist in NzbVortex. Go to NzbVortex to create it."
};
}
}
if (!Settings.Category.IsNullOrWhiteSpace() && !groups.Any(v => v.GroupName == Settings.Category))
{
return new NzbDroneValidationFailure("Category", "Category does not exist")
{
DetailedDescription = "The category you entered doesn't exist in NzbVortex. Go to NzbVortex to create it."
};
}
return null;
}

View File

@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public interface INzbVortexProxy
{
string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings);
string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings, string group);
void Remove(int id, bool deleteData, NzbVortexSettings settings);
NzbVortexVersionResponse GetVersion(NzbVortexSettings settings);
NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings);
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
_authSessionIdCache = cacheManager.GetCache<string>(GetType(), "authCache");
}
public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings)
public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings, string group)
{
var requestBuilder = BuildRequest(settings).Resource("nzb/add")
.Post()
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
if (settings.Category.IsNotNullOrWhiteSpace())
{
requestBuilder.AddQueryParam("groupname", settings.Category);
requestBuilder.AddQueryParam("groupname", group);
}
requestBuilder.AddFormUpload("name", filename, nzbData, "application/x-nzb");

View File

@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
[FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }
[FieldDefinition(4, Label = "Group", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(4, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
public string Category { get; set; }
[FieldDefinition(5, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing items")]

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContent)
{
var category = Settings.Category;
var category = GetCategoryForRelease(release) ?? Settings.Category;
var priority = Settings.Priority;
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
protected override string AddFromLink(ReleaseInfo release)
{
var category = Settings.Category;
var category = GetCategoryForRelease(release) ?? Settings.Category;
var priority = Settings.Priority;
@@ -66,6 +66,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
}
public override string Name => "NZBGet";
public override bool SupportsCategories => true;
protected IEnumerable<NzbgetCategory> GetCategories(Dictionary<string, string> config)
{
@@ -139,6 +140,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var config = _proxy.GetConfig(Settings);
var categories = GetCategories(config);
foreach (var category in Categories)
{
if (!category.ClientCategory.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == category.ClientCategory))
{
return new NzbDroneValidationFailure(string.Empty, "Category does not exist")
{
InfoLink = _proxy.GetBaseUrl(Settings),
DetailedDescription = "A mapped category you entered doesn't exist in NZBGet. Go to NZBGet to create it."
};
}
}
if (!Settings.Category.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == Settings.Category))
{
return new NzbDroneValidationFailure("Category", "Category does not exist")

View File

@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
public string Category { get; set; }
[FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority for items added from Prowlarr")]

View File

@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
}
public override string Name => "Pneumatic";
public override bool SupportsCategories => false;
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;

View File

@@ -52,8 +52,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
//var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
var itemToTop = Settings.Priority == (int)QBittorrentPriority.First;
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
var category = GetCategoryForRelease(release) ?? Settings.Category;
Proxy.AddTorrentFromUrl(magnetLink, null, Settings);
Proxy.AddTorrentFromUrl(magnetLink, null, Settings, category);
if (itemToTop || forceStart)
{
@@ -100,8 +101,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
//var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
var itemToTop = Settings.Priority == (int)QBittorrentPriority.First;
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
var category = GetCategoryForRelease(release) ?? Settings.Category;
Proxy.AddTorrentFromFile(filename, fileContent, null, Settings);
Proxy.AddTorrentFromFile(filename, fileContent, null, Settings, category);
if (itemToTop || forceStart)
{
@@ -167,6 +169,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
public override string Name => "qBittorrent";
public override bool SupportsCategories => true;
protected override void Test(List<ValidationFailure> failures)
{
@@ -197,7 +200,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
else if (version < Version.Parse("1.6"))
{
// API version 6 introduced support for labels
if (Settings.Category.IsNotNullOrWhiteSpace())
if (Settings.Category.IsNotNullOrWhiteSpace() || Categories.Count > 0)
{
return new NzbDroneValidationFailure("Category", "Category is not supported")
{
@@ -205,15 +208,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
};
}
}
else if (Settings.Category.IsNullOrWhiteSpace())
{
// warn if labels are supported, but category is not provided
return new NzbDroneValidationFailure("Category", "Category is recommended")
{
IsWarning = true,
DetailedDescription = "Prowlarr will not attempt to import completed downloads without a category."
};
}
}
catch (DownloadClientAuthenticationException ex)
{
@@ -251,7 +245,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
private ValidationFailure TestCategory()
{
if (Settings.Category.IsNullOrWhiteSpace())
if (Settings.Category.IsNullOrWhiteSpace() && Categories.Count == 0)
{
return null;
}
@@ -265,6 +259,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Dictionary<string, QBittorrentLabel> labels = Proxy.GetLabels(Settings);
foreach (var category in Categories)
{
if (category.ClientCategory.IsNotNullOrWhiteSpace() && !labels.ContainsKey(category.ClientCategory))
{
Proxy.AddLabel(category.ClientCategory, Settings);
labels = Proxy.GetLabels(Settings);
if (!labels.ContainsKey(category.ClientCategory))
{
return new NzbDroneValidationFailure(string.Empty, "Configuration of label failed")
{
DetailedDescription = "Prowlarr was unable to add the label to qBittorrent."
};
}
}
}
if (Settings.Category.IsNotNullOrWhiteSpace() && !labels.ContainsKey(Settings.Category))
{
Proxy.AddLabel(Settings.Category, Settings);

View File

@@ -18,8 +18,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings);
List<QBittorrentTorrentFile> GetTorrentFiles(string hash, QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category);
void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category);
void RemoveTorrent(string hash, bool removeData, QBittorrentSettings settings);
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);

View File

@@ -113,15 +113,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response;
}
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category)
{
var request = BuildRequest(settings).Resource("/command/download")
.Post()
.AddFormParameter("urls", torrentUrl);
if (settings.Category.IsNotNullOrWhiteSpace())
if (category.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.Category);
request.AddFormParameter("category", category);
}
// Note: ForceStart is handled by separate api call
@@ -143,15 +143,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category)
{
var request = BuildRequest(settings).Resource("/command/upload")
.Post()
.AddFormUpload("torrents", fileName, fileContent);
if (settings.Category.IsNotNullOrWhiteSpace())
if (category.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.Category);
request.AddFormParameter("category", category);
}
// Note: ForceStart is handled by separate api call

View File

@@ -119,14 +119,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response;
}
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post()
.AddFormParameter("urls", torrentUrl);
if (settings.Category.IsNotNullOrWhiteSpace())
if (category.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.Category);
request.AddFormParameter("category", category);
}
// Note: ForceStart is handled by separate api call
@@ -153,15 +153,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post()
.AddFormUpload("torrents", fileName, fileContent);
if (settings.Category.IsNotNullOrWhiteSpace())
if (category.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.Category);
request.AddFormParameter("category", category);
}
// Note: ForceStart is handled by separate api call

View File

@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
public string Category { get; set; }
[FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing items")]

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContent)
{
var category = Settings.Category;
var category = GetCategoryForRelease(release) ?? Settings.Category;
var priority = Settings.Priority;
var response = _proxy.DownloadNzb(fileContent, filename, category, priority, Settings);
@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
protected override string AddFromLink(ReleaseInfo release)
{
var category = Settings.Category;
var category = GetCategoryForRelease(release) ?? Settings.Category;
var priority = Settings.Priority;
var response = _proxy.DownloadNzbByUrl(release.DownloadUrl, category, priority, Settings);
@@ -62,6 +62,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
}
public override string Name => "SABnzbd";
public override bool SupportsCategories => true;
protected IEnumerable<SabnzbdCategory> GetCategories(SabnzbdConfig config)
{
@@ -260,29 +261,27 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
private ValidationFailure TestCategory()
{
var config = _proxy.GetConfig(Settings);
var category = GetCategories(config).FirstOrDefault((SabnzbdCategory v) => v.Name == Settings.Category);
var categories = GetCategories(config);
if (category != null)
foreach (var category in Categories)
{
if (category.Dir.EndsWith("*"))
if (!category.ClientCategory.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == category.ClientCategory))
{
return new NzbDroneValidationFailure("Category", "Enable Job folders")
return new NzbDroneValidationFailure(string.Empty, "Category does not exist")
{
InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"),
DetailedDescription = "Prowlarr prefers each download to have a separate folder. With * appended to the Folder/Path SABnzbd will not create these job folders. Go to SABnzbd to fix it."
InfoLink = _proxy.GetBaseUrl(Settings),
DetailedDescription = "A mapped category you entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it."
};
}
}
else
if (!Settings.Category.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == Settings.Category))
{
if (!Settings.Category.IsNullOrWhiteSpace())
return new NzbDroneValidationFailure("Category", "Category does not exist")
{
return new NzbDroneValidationFailure("Category", "Category does not exist")
{
InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"),
DetailedDescription = "The category you entered doesn't exist in SABnzbd. Go to SABnzbd to create it."
};
}
InfoLink = _proxy.GetBaseUrl(Settings),
DetailedDescription = "The category you entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it."
};
}
if (config.Misc.enable_tv_sorting && ContainsCategory(config.Misc.tv_categories, Settings.Category))

View File

@@ -65,7 +65,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
[FieldDefinition(6, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(7, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
public string Category { get; set; }
[FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing items")]

View File

@@ -38,5 +38,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission
}
public override string Name => "Transmission";
public override bool SupportsCategories => false;
}
}

View File

@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
public string Category { get; set; }
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")]

View File

@@ -58,5 +58,6 @@ namespace NzbDrone.Core.Download.Clients.Vuze
}
public override string Name => "Vuze";
public override bool SupportsCategories => false;
}
}

View File

@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
{
var priority = (RTorrentPriority)Settings.Priority;
_proxy.AddTorrentFromUrl(magnetLink, Settings.Category, priority, Settings.Directory, Settings);
_proxy.AddTorrentFromUrl(magnetLink, GetCategoryForRelease(release) ?? Settings.Category, priority, Settings.Directory, Settings);
var tries = 10;
var retryDelay = 500;
@@ -58,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
{
var priority = (RTorrentPriority)Settings.Priority;
_proxy.AddTorrentFromFile(filename, fileContent, Settings.Category, priority, Settings.Directory, Settings);
_proxy.AddTorrentFromFile(filename, fileContent, GetCategoryForRelease(release) ?? Settings.Category, priority, Settings.Directory, Settings);
var tries = 10;
var retryDelay = 500;
@@ -73,6 +73,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
}
public override string Name => "rTorrent";
public override bool SupportsCategories => true;
public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to remove torrents that have finished seeding when using rTorrent", ProviderMessageType.Warning);

View File

@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional.")]
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional.")]
public string Category { get; set; }
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")]

View File

@@ -38,9 +38,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
_proxy.AddTorrentFromUrl(magnetLink, Settings);
//_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
if (Settings.Category.IsNotNullOrWhiteSpace())
var category = GetCategoryForRelease(release) ?? Settings.Category;
if (GetCategoryForRelease(release).IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash, Settings.Category, Settings);
_proxy.SetTorrentLabel(hash, category, Settings);
}
if (Settings.Priority == (int)UTorrentPriority.First)
@@ -58,9 +59,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
//_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
if (Settings.Category.IsNotNullOrWhiteSpace())
var category = GetCategoryForRelease(release) ?? Settings.Category;
if (category.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash, Settings.Category, Settings);
_proxy.SetTorrentLabel(hash, category, Settings);
}
if (Settings.Priority == (int)UTorrentPriority.First)
@@ -74,40 +76,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
}
public override string Name => "uTorrent";
private List<UTorrentTorrent> GetTorrents()
{
List<UTorrentTorrent> torrents;
var cacheKey = string.Format("{0}:{1}:{2}", Settings.Host, Settings.Port, Settings.Category);
var cache = _torrentCache.Find(cacheKey);
var response = _proxy.GetTorrents(cache == null ? null : cache.CacheID, Settings);
if (cache != null && response.Torrents == null)
{
var removedAndUpdated = new HashSet<string>(response.TorrentsChanged.Select(v => v.Hash).Concat(response.TorrentsRemoved));
torrents = cache.Torrents
.Where(v => !removedAndUpdated.Contains(v.Hash))
.Concat(response.TorrentsChanged)
.ToList();
}
else
{
torrents = response.Torrents;
}
cache = new UTorrentTorrentCache
{
CacheID = response.CacheNumber,
Torrents = torrents
};
_torrentCache.Set(cacheKey, cache, TimeSpan.FromMinutes(15));
return torrents;
}
public override bool SupportsCategories => true;
protected override void Test(List<ValidationFailure> failures)
{

View File

@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
public string Category { get; set; }
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing items")]

View File

@@ -1,14 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using Org.BouncyCastle.Crypto.Tls;
namespace NzbDrone.Core.Download
{
@@ -50,6 +53,9 @@ namespace NzbDrone.Core.Download
return GetType().Name;
}
protected List<DownloadClientCategory> Categories => ((DownloadClientDefinition)Definition).Categories;
public abstract bool SupportsCategories { get; }
public abstract DownloadProtocol Protocol
{
get;
@@ -57,12 +63,54 @@ namespace NzbDrone.Core.Download
public abstract Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer);
protected string GetCategoryForRelease(ReleaseInfo release)
{
var categories = ((DownloadClientDefinition)Definition).Categories;
if (categories.Count == 0)
{
return null;
}
// Check for direct mapping
var category = categories.FirstOrDefault(x => x.Categories.Intersect(release.Categories.Select(c => c.Id)).Any())?.ClientCategory;
// Check for parent mapping
if (category == null)
{
foreach (var cat in categories)
{
var mappedCat = NewznabStandardCategory.AllCats.Where(x => cat.Categories.Contains(x.Id));
var subCats = mappedCat.SelectMany(x => x.SubCategories);
if (subCats.Intersect(release.Categories).Any())
{
category = cat.ClientCategory;
break;
}
}
}
return category;
}
protected virtual void ValidateCategories(List<ValidationFailure> failures)
{
foreach (var category in ((DownloadClientDefinition)Definition).Categories)
{
if (category.ClientCategory.IsNullOrWhiteSpace())
{
failures.AddIfNotNull(new ValidationFailure(string.Empty, "Category can not be empty"));
}
}
}
public ValidationResult Test()
{
var failures = new List<ValidationFailure>();
try
{
ValidateCategories(failures);
Test(failures);
}
catch (Exception ex)
@@ -97,5 +145,17 @@ namespace NzbDrone.Core.Download
return null;
}
private bool HasConcreteImplementation(string methodName)
{
var method = GetType().GetMethod(methodName);
if (method == null)
{
throw new MissingMethodException(GetType().Name, Name);
}
return !method.DeclaringType.IsAbstract;
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NzbDrone.Core.Download
{
public class DownloadClientCategory
{
public string ClientCategory { get; set; }
public List<int> Categories { get; set; }
}
}

View File

@@ -1,10 +1,18 @@
using NzbDrone.Core.Indexers;
using System.Collections.Generic;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Download
{
public class DownloadClientDefinition : ProviderDefinition
{
public DownloadClientDefinition()
{
Categories = new List<DownloadClientCategory>();
}
public List<DownloadClientCategory> Categories { get; set; }
public bool SupportsCategories { get; set; }
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; } = 1;
}

View File

@@ -40,6 +40,7 @@ namespace NzbDrone.Core.Download
base.SetProviderCharacteristics(provider, definition);
definition.Protocol = provider.Protocol;
definition.SupportsCategories = provider.SupportsCategories;
}
public List<IDownloadClient> DownloadHandlingEnabled(bool filterBlockedClients = true)

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
@@ -7,6 +8,7 @@ namespace NzbDrone.Core.Download
{
public interface IDownloadClient : IProvider
{
bool SupportsCategories { get; }
DownloadProtocol Protocol { get; }
Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer);
}

View File

@@ -8,6 +8,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerCheck : HealthCheckBase
{

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerLongTermStatusCheck : HealthCheckBase
{

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerStatusCheck : HealthCheckBase
{

View File

@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
public class IndexerVIPCheck : HealthCheckBase
{
private readonly IIndexerFactory _indexerFactory;

View File

@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
public class IndexerVIPExpiredCheck : HealthCheckBase
{
private readonly IIndexerFactory _indexerFactory;

View File

@@ -9,6 +9,7 @@ using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
public class NoDefinitionCheck : HealthCheckBase
{
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;

View File

@@ -9,6 +9,7 @@ using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
public class OutdatedDefinitionCheck : HealthCheckBase
{
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;

View File

@@ -56,9 +56,10 @@ namespace NzbDrone.Core.IndexerStats
.ToArray();
int temp = 0;
indexerStats.AverageResponseTime = (int)sortedEvents.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp))
.Select(h => temp)
.Average();
var elapsedTimeEvents = sortedEvents.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp))
.Select(h => temp);
indexerStats.AverageResponseTime = elapsedTimeEvents.Count() > 0 ? (int)elapsedTimeEvents.Average() : 0;
foreach (var historyEvent in sortedEvents)
{

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public AvistazIdInfo MovieTvinfo { get; set; }
[JsonProperty(PropertyName = "created_at")]
public DateTime CreatedAt { get; set; }
public string CreatedAt { get; set; }
[JsonProperty(PropertyName = "file_name")]
public string FileName { get; set; }

View File

@@ -61,7 +61,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
InfoUrl = details,
Guid = details,
Categories = cats,
PublishDate = row.CreatedAt,
PublishDate = DateTime.Parse(row.CreatedAt + "-05:00").ToUniversalTime(), // Avistaz does not specify a timezone & returns server time
Size = row.FileSize,
Files = row.FileCount,
Grabs = row.Completed,

View File

@@ -1,22 +1,14 @@
using System.Collections.Generic;
using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Cardigann
{
public class CardigannSettingsValidator : AbstractValidator<CardigannSettings>
{
public CardigannSettingsValidator()
{
}
}
public class CardigannSettings : NoAuthTorrentBaseSettings
{
private static readonly CardigannSettingsValidator Validator = new CardigannSettingsValidator();
public CardigannSettings()
{
ExtraFieldData = new Dictionary<string, object>();
@@ -26,10 +18,5 @@ namespace NzbDrone.Core.Indexers.Cardigann
public string DefinitionFile { get; set; }
public Dictionary<string, object> ExtraFieldData { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -109,6 +109,11 @@ namespace NzbDrone.Core.Indexers.FileList
var baseUrl = string.Format("{0}/api.php?action={1}&category={2}&username={3}&passkey={4}{5}", Settings.BaseUrl.TrimEnd('/'), searchType, categoriesQuery, Settings.Username.Trim(), Settings.Passkey.Trim(), parameters);
if (Settings.FreeleechOnly)
{
baseUrl += "&freeleech=1";
}
yield return new IndexerRequest(baseUrl, HttpAccept.Json);
}
}

View File

@@ -29,6 +29,9 @@ namespace NzbDrone.Core.Indexers.FileList
[FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey (This is the alphanumeric string in the tracker url shown in your download client)", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Passkey { get; set; }
[FieldDefinition(4, Label = "Freeleech Only", HelpText = "Search Freeleech torrents only", Type = FieldType.Checkbox)]
public bool FreeleechOnly { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -10,6 +10,7 @@ namespace NzbDrone.Core.Indexers.Headphones
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
}
}

View File

@@ -1,23 +1,19 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using FluentValidation;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
@@ -25,7 +21,6 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "Nebulance";
public override string[] IndexerUrls => new string[] { "https://nebulance.io/" };
private string LoginUrl => Settings.BaseUrl + "login.php";
public override string Description => "Nebulance (NBL) is a ratioless Private Torrent Tracker for TV";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
@@ -48,53 +43,13 @@ namespace NzbDrone.Core.Indexers.Definitions
return new NebulanceParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
{
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true
};
requestBuilder.Method = HttpMethod.Post;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var cookies = Cookies;
Cookies = null;
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("twofa", Settings.TwoFactorAuth)
.AddFormParameter("keeplogged", "on")
.AddFormParameter("login", "Login")
.SetHeader("Content-Type", "multipart/form-data")
.Build();
var response = await ExecuteAuth(authLoginRequest);
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
_logger.Debug("Nebulance authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (!httpResponse.Content.Contains("logout.php"))
{
return true;
}
return false;
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
}
};
@@ -115,30 +70,16 @@ namespace NzbDrone.Core.Indexers.Definitions
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
private IEnumerable<IndexerRequest> GetPagedRequests(NebulanceQuery parameters, int? results, int? offset)
{
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
var apiUrl = Settings.BaseUrl + "api.php";
var searchTerm = term;
var builder = new JsonRpcRequestBuilder(apiUrl)
.Call("getTorrents", Settings.ApiKey, parameters, results ?? 100, offset ?? 0);
if (!string.IsNullOrWhiteSpace(searchTerm))
{
searchTerm = Regex.Replace(searchTerm, @"[-._]", " ");
}
builder.SuppressHttpError = true;
var qc = new NameValueCollection
{
{ "action", "basic" },
{ "order_by", "time" },
{ "order_way", "desc" },
{ "searchtext", searchTerm }
};
searchUrl = searchUrl + "?" + qc.GetQueryString();
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
yield return new IndexerRequest(builder.Build());
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
@@ -159,7 +100,27 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
var queryParams = new NebulanceQuery
{
Age = ">0"
};
if (searchCriteria.SanitizedTvSearchString.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + searchCriteria.SanitizedTvSearchString + "%";
}
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.ImdbId, out var intImdb))
{
queryParams.Imdb = intImdb;
if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + searchCriteria.EpisodeSearchString + "%";
}
}
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
return pageableRequests;
}
@@ -175,7 +136,17 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
var queryParams = new NebulanceQuery
{
Age = ">0"
};
if (searchCriteria.SanitizedSearchTerm.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + searchCriteria.SanitizedSearchTerm + "%";
}
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
return pageableRequests;
}
@@ -199,60 +170,38 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var torrentInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var document = parser.ParseDocument(indexerResponse.Content);
var rows = document.QuerySelectorAll(".torrent_table > tbody > tr[class^='torrent row']");
JsonRpcResponse<NebulanceTorrents> jsonResponse = new HttpResponse<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse).Resource;
if (jsonResponse.Error != null || jsonResponse.Result == null)
{
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error);
}
if (jsonResponse.Result.Items.Count == 0)
{
return torrentInfos;
}
var rows = jsonResponse.Result.Items;
foreach (var row in rows)
{
var title = row.QuerySelector("a[data-src]").GetAttribute("data-src");
if (string.IsNullOrEmpty(title) || title == "0")
{
title = row.QuerySelector("a[data-src]").TextContent;
title = Regex.Replace(title, @"[\[\]\/]", "");
}
else
{
if (title.Length > 5 && title.Substring(title.Length - 5).Contains("."))
{
title = title.Remove(title.LastIndexOf(".", StringComparison.Ordinal));
}
}
var posterStr = row.QuerySelector("img")?.GetAttribute("src");
Uri.TryCreate(posterStr, UriKind.Absolute, out var poster);
var details = _settings.BaseUrl + row.QuerySelector("a[data-src]").GetAttribute("href");
var link = _settings.BaseUrl + row.QuerySelector("a[href*='action=download']").GetAttribute("href");
var qColSize = row.QuerySelector("td:nth-child(3)");
var size = ParseUtil.GetBytes(qColSize.Children[0].TextContent);
var files = ParseUtil.CoerceInt(qColSize.Children[1].TextContent.Split(':')[1].Trim());
var qPublishdate = row.QuerySelector("td:nth-child(4) span");
var publishDateStr = qPublishdate.GetAttribute("title");
var publishDate = !string.IsNullOrEmpty(publishDateStr) && publishDateStr.Contains(",")
? DateTime.ParseExact(publishDateStr, "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture)
: DateTime.ParseExact(qPublishdate.TextContent.Trim(), "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture);
var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(5)").TextContent);
var seeds = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent);
var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent);
var details = _settings.BaseUrl + "torrents.php?id=" + row.TorrentId;
var release = new TorrentInfo
{
Title = title,
Title = row.ReleaseTitle,
Guid = details,
InfoUrl = details,
PosterUrl = poster?.AbsoluteUri ?? null,
DownloadUrl = link,
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(title) },
Size = size,
Files = files,
PublishDate = publishDate,
Grabs = grabs,
Seeders = seeds,
Peers = seeds + leechers,
PosterUrl = row.Banner,
DownloadUrl = row.Download,
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) },
Size = ParseUtil.CoerceLong(row.Size),
Files = row.FileList.Length,
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal),
Grabs = ParseUtil.CoerceInt(row.Snatch),
Seeders = ParseUtil.CoerceInt(row.Seed),
Peers = ParseUtil.CoerceInt(row.Seed) + ParseUtil.CoerceInt(row.Leech),
MinimumRatio = 0, // ratioless
MinimumSeedTime = 86400, // 24 hours
DownloadVolumeFactor = 0, // ratioless tracker
@@ -268,14 +217,69 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class NebulanceSettings : UserPassTorrentBaseSettings
public class NebulanceSettings : NoAuthTorrentBaseSettings
{
public NebulanceSettings()
{
TwoFactorAuth = "";
ApiKey = "";
}
[FieldDefinition(4, Label = "Two Factor Auth", HelpText = "Two-Factor Auth")]
public string TwoFactorAuth { get; set; }
[FieldDefinition(4, Label = "API Key", HelpText = "API Key from User Settings > Api Keys. Key must have List and Download permissions")]
public string ApiKey { get; set; }
}
public class NebulanceQuery
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Id { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Time { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Age { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int Tvmaze { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int Imdb { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Hash { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string[] Tags { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Name { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Category { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Series { get; set; }
public NebulanceQuery Clone()
{
return MemberwiseClone() as NebulanceQuery;
}
}
public class NebulanceTorrent
{
[JsonProperty(PropertyName = "rls_name")]
public string ReleaseTitle { get; set; }
public string Title { get; set; }
public string Size { get; set; }
public string Seed { get; set; }
public string Leech { get; set; }
public string Snatch { get; set; }
public string Download { get; set; }
[JsonProperty(PropertyName = "file_list")]
public string[] FileList { get; set; }
[JsonProperty(PropertyName = "series_banner")]
public string Banner { get; set; }
[JsonProperty(PropertyName = "group_id")]
public string TorrentId { get; set; }
[JsonProperty(PropertyName = "rls_utc")]
public string PublishDateUtc { get; set; }
}
public class NebulanceTorrents
{
public List<NebulanceTorrent> Items { get; set; }
public int Results { get; set; }
}
}

View File

@@ -42,7 +42,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public override IParseIndexerResponse GetParser()
{
return new NewznabRssParser(Settings);
return new NewznabRssParser(Settings, Definition, _capabilitiesProvider);
}
public string[] GetBaseUrlFromSettings()
@@ -181,13 +181,13 @@ namespace NzbDrone.Core.Indexers.Newznab
}
if (capabilities.MovieSearchParams != null &&
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId, MovieSearchParam.TraktId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
{
return null;
}
if (capabilities.TvSearchParams != null &&
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.ImdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
new[] { TvSearchParam.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
{
return null;

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Xml;
using System.Xml.Linq;
@@ -221,27 +222,59 @@ namespace NzbDrone.Core.Indexers.Newznab
{
foreach (var xmlCategory in xmlCategories.Elements("category"))
{
var cat = new IndexerCategory
var parentName = xmlCategory.Attribute("name").Value;
var parentId = int.Parse(xmlCategory.Attribute("id").Value);
var mappedCat = NewznabStandardCategory.ParentCats.FirstOrDefault(x => parentName.ToLower().Contains(x.Name.ToLower()));
if (mappedCat == null)
{
Id = int.Parse(xmlCategory.Attribute("id").Value),
Name = xmlCategory.Attribute("name").Value,
Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty
};
// Try by parent id if name fails
mappedCat = NewznabStandardCategory.ParentCats.FirstOrDefault(x => x.Id == parentId);
}
if (mappedCat == null)
{
// Fallback to Other
mappedCat = NewznabStandardCategory.Other;
}
foreach (var xmlSubcat in xmlCategory.Elements("subcat"))
{
var subCat = new IndexerCategory
{
Id = int.Parse(xmlSubcat.Attribute("id").Value),
Name = xmlSubcat.Attribute("name").Value,
Description = xmlSubcat.Attribute("description") != null ? xmlSubcat.Attribute("description").Value : string.Empty
};
var subName = xmlSubcat.Attribute("name").Value;
var subId = int.Parse(xmlSubcat.Attribute("id").Value);
cat.SubCategories.Add(subCat);
capabilities.Categories.AddCategoryMapping(subCat.Name, subCat);
var mappingName = $"{mappedCat.Name}/{subName}";
var mappedSubCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Name.ToLower() == mappingName.ToLower());
if (mappedSubCat == null)
{
// Try by child id if name fails
mappedSubCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Id == subId);
}
if (mappedSubCat == null && mappedCat.Id != NewznabStandardCategory.Other.Id)
{
// Try by Parent/Other if parent is not other
mappedSubCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Name.ToLower() == $"{mappedCat.Name.ToLower()}/other");
}
if (mappedSubCat == null)
{
// Fallback to Misc Other
mappedSubCat = NewznabStandardCategory.OtherMisc;
}
if (mappedSubCat != null)
{
capabilities.Categories.AddCategoryMapping(subId, mappedSubCat, $"{parentName}/{subName}");
}
}
capabilities.Categories.AddCategoryMapping(cat.Name, cat);
if (mappedCat != null)
{
capabilities.Categories.AddCategoryMapping(parentId, mappedCat, parentName);
}
}
}

View File

@@ -68,6 +68,7 @@ namespace NzbDrone.Core.Indexers.Newznab
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
capabilities,
parameters));
return pageableRequests;
@@ -109,6 +110,7 @@ namespace NzbDrone.Core.Indexers.Newznab
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
capabilities,
parameters));
return pageableRequests;
@@ -175,6 +177,7 @@ namespace NzbDrone.Core.Indexers.Newznab
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
capabilities,
parameters));
return pageableRequests;
@@ -216,6 +219,7 @@ namespace NzbDrone.Core.Indexers.Newznab
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
capabilities,
parameters));
return pageableRequests;
@@ -233,15 +237,15 @@ namespace NzbDrone.Core.Indexers.Newznab
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, IndexerCapabilities capabilities, NameValueCollection parameters)
{
var baseUrl = string.Format("{0}{1}?t={2}&extended=1", Settings.BaseUrl.TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchCriteria.SearchType);
var categories = searchCriteria.Categories;
var categories = capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
if (categories != null && categories.Any())
{

View File

@@ -5,6 +5,7 @@ using System.Xml.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers.Newznab
{
@@ -13,12 +14,16 @@ namespace NzbDrone.Core.Indexers.Newznab
public const string ns = "{http://www.newznab.com/DTD/2010/feeds/attributes/}";
private readonly NewznabSettings _settings;
private readonly ProviderDefinition _definition;
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public NewznabRssParser(NewznabSettings settings)
public NewznabRssParser(NewznabSettings settings, ProviderDefinition definition, INewznabCapabilitiesProvider capabilitiesProvider)
{
PreferredEnclosureMimeTypes = UsenetEnclosureMimeTypes;
UseEnclosureUrl = true;
_settings = settings;
_definition = definition;
_capabilitiesProvider = capabilitiesProvider;
}
public static void CheckError(XDocument xdoc, IndexerResponse indexerResponse)
@@ -118,19 +123,17 @@ namespace NzbDrone.Core.Indexers.Newznab
protected override ICollection<IndexerCategory> GetCategory(XElement item)
{
var capabilities = _capabilitiesProvider.GetCapabilities(_settings, _definition);
var cats = TryGetMultipleNewznabAttributes(item, "category");
var results = new List<IndexerCategory>();
foreach (var cat in cats)
{
if (int.TryParse(cat, out var intCategory))
{
var indexerCat = _settings.Categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
var indexerCat = capabilities.Categories.MapTrackerCatToNewznab(cat);
if (indexerCat != null)
{
results.Add(indexerCat);
}
if (indexerCat != null)
{
results.AddRange(indexerCat);
}
}

View File

@@ -35,6 +35,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public NewznabSettingsValidator()
{
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(c => c.BaseUrl).ValidRootUrl();
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);

View File

@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Torznab
public override IParseIndexerResponse GetParser()
{
return new TorznabRssParser(Settings);
return new TorznabRssParser(Settings, Definition, _capabilitiesProvider);
}
public string[] GetBaseUrlFromSettings()
@@ -157,13 +157,13 @@ namespace NzbDrone.Core.Indexers.Torznab
}
if (capabilities.MovieSearchParams != null &&
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId, MovieSearchParam.TraktId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
{
return null;
}
if (capabilities.TvSearchParams != null &&
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.ImdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
new[] { TvSearchParam.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
{
return null;

View File

@@ -4,7 +4,9 @@ using System.Linq;
using System.Xml.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers.Torznab
{
@@ -13,10 +15,15 @@ namespace NzbDrone.Core.Indexers.Torznab
public const string ns = "{http://torznab.com/schemas/2015/feed}";
private readonly TorznabSettings _settings;
public TorznabRssParser(TorznabSettings settings)
private readonly ProviderDefinition _definition;
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public TorznabRssParser(TorznabSettings settings, ProviderDefinition definition, INewznabCapabilitiesProvider capabilitiesProvider)
{
UseEnclosureUrl = true;
_settings = settings;
_definition = definition;
_capabilitiesProvider = capabilitiesProvider;
}
protected override bool PreProcess(IndexerResponse indexerResponse)
@@ -157,19 +164,17 @@ namespace NzbDrone.Core.Indexers.Torznab
protected override ICollection<IndexerCategory> GetCategory(XElement item)
{
var capabilities = _capabilitiesProvider.GetCapabilities(_settings, _definition);
var cats = TryGetMultipleNewznabAttributes(item, "category");
var results = new List<IndexerCategory>();
foreach (var cat in cats)
{
if (int.TryParse(cat, out var intCategory))
{
var indexerCat = _settings.Categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
var indexerCat = capabilities.Categories.MapTrackerCatToNewznab(cat);
if (indexerCat != null)
{
results.Add(indexerCat);
}
if (indexerCat != null)
{
results.AddRange(indexerCat);
}
}

View File

@@ -29,6 +29,8 @@ namespace NzbDrone.Core.Indexers.Torznab
public TorznabSettingsValidator()
{
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
RuleFor(c => c.BaseUrl).ValidRootUrl();
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);

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