Compare commits

...

7 Commits

Author SHA1 Message Date
Qstick
0c45eb68fa Newznab to Yml 2022-12-17 17:53: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
93 changed files with 1945 additions and 458 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

@@ -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,19 +62,27 @@ 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;
console.log(supportsCategories);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -136,6 +169,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 +255,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

@@ -0,0 +1,171 @@
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);
console.log(saveData);
// 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

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

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

@@ -15,14 +15,14 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[TestFixture]
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
{
private NewznabSettings _settings;
private GenericNewznabSettings _settings;
private IndexerDefinition _definition;
private string _caps;
[SetUp]
public void SetUp()
{
_settings = new NewznabSettings()
_settings = new GenericNewznabSettings()
{
BaseUrl = "http://indxer.local"
};

View File

@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps);
}

View File

@@ -10,7 +10,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
public class NewznabRequestGeneratorFixture : CoreTest<GenericNewznabRequestGenerator>
{
private MovieSearchCriteria _movieSearchCriteria;
private TvSearchCriteria _tvSearchCriteria;
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[SetUp]
public void SetUp()
{
Subject.Settings = new NewznabSettings()
Subject.Settings = new GenericNewznabSettings()
{
BaseUrl = "http://127.0.0.1:1234/",
ApiKey = "abcd",
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_capabilities = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_capabilities);
}

View File

@@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
_caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps);
}

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

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(24)]
public class newznab_yml : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Update.Table("Indexers").Set(new { Implementation = "GenericNewznab", ConfigContract = "GenericNewznabSettings" }).Where(new { Implementation = "Newznab" });
}
}
}

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

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

@@ -19,7 +19,8 @@ namespace NzbDrone.Core.IndexerVersions
{
public interface IIndexerDefinitionUpdateService
{
List<CardigannMetaDefinition> All();
List<IndexerMetaDefinition> All();
List<IndexerMetaDefinition> AllForImplementation(string implementation);
CardigannDefinition GetCachedDefinition(string fileKey);
List<string> GetBlocklist();
}
@@ -28,8 +29,8 @@ namespace NzbDrone.Core.IndexerVersions
{
/* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 7;
private const string DEFINITION_BRANCH = "newznab-yml";
private const int DEFINITION_VERSION = 8;
//Used when moving yml to C#
private readonly List<string> _defintionBlocklist = new List<string>()
@@ -78,9 +79,9 @@ namespace NzbDrone.Core.IndexerVersions
_logger = logger;
}
public List<CardigannMetaDefinition> All()
public List<IndexerMetaDefinition> All()
{
var indexerList = new List<CardigannMetaDefinition>();
var indexerList = new List<IndexerMetaDefinition>();
try
{
@@ -88,7 +89,7 @@ namespace NzbDrone.Core.IndexerVersions
try
{
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
var response = _httpClient.Get<List<IndexerMetaDefinition>>(request);
indexerList = response.Resource.Where(i => !_defintionBlocklist.Contains(i.File)).ToList();
}
catch
@@ -111,6 +112,11 @@ namespace NzbDrone.Core.IndexerVersions
return indexerList;
}
public List<IndexerMetaDefinition> AllForImplementation(string implementation)
{
return All().Where(d => d.Implementation == implementation.ToLower()).ToList();
}
public CardigannDefinition GetCachedDefinition(string fileKey)
{
if (string.IsNullOrEmpty(fileKey))
@@ -128,7 +134,7 @@ namespace NzbDrone.Core.IndexerVersions
return _defintionBlocklist;
}
private List<CardigannMetaDefinition> ReadDefinitionsFromDisk(List<CardigannMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
private List<IndexerMetaDefinition> ReadDefinitionsFromDisk(List<IndexerMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
{
var indexerList = defs;
@@ -145,7 +151,7 @@ namespace NzbDrone.Core.IndexerVersions
try
{
var definitionString = File.ReadAllText(file.FullName);
var definition = _deserializer.Deserialize<CardigannMetaDefinition>(definitionString);
var definition = _deserializer.Deserialize<IndexerMetaDefinition>(definitionString);
definition.File = Path.GetFileNameWithoutExtension(file.Name);
@@ -243,6 +249,11 @@ namespace NzbDrone.Core.IndexerVersions
definition.Login.Method = "form";
}
if (definition.Search == null)
{
definition.Search = new SearchBlock();
}
if (definition.Search.Paths == null)
{
definition.Search.Paths = new List<SearchPathBlock>();

View File

@@ -1,10 +1,11 @@
using System.Collections.Generic;
using NzbDrone.Core.Indexers.Cardigann;
namespace NzbDrone.Core.Indexers.Cardigann
namespace NzbDrone.Core.IndexerVersions
{
public class CardigannMetaDefinition
public class IndexerMetaDefinition
{
public CardigannMetaDefinition()
public IndexerMetaDefinition()
{
Legacylinks = new List<string>();
}
@@ -13,6 +14,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
public string File { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Implementation { get; set; }
public string Type { get; set; }
public string Language { get; set; }
public string Encoding { get; set; }

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

@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
get
{
foreach (var def in _definitionService.All())
foreach (var def in _definitionService.AllForImplementation(GetType().Name))
{
yield return GetDefinition(def);
}
@@ -98,7 +98,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
_generatorCache = cacheManager.GetRollingCache<CardigannRequestGenerator>(GetType(), "CardigannGeneratorCache", TimeSpan.FromMinutes(5));
}
private IndexerDefinition GetDefinition(CardigannMetaDefinition definition)
private IndexerDefinition GetDefinition(IndexerMetaDefinition definition)
{
var defaultSettings = new List<SettingsField>
{

View File

@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
}
public class CardigannSettings : NoAuthTorrentBaseSettings
public class CardigannSettings : NoAuthTorrentBaseSettings, IYmlIndexerSettings
{
private static readonly CardigannSettingsValidator Validator = new CardigannSettingsValidator();

View File

@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Newznab
{
public class GenericNewznab : UsenetIndexerBase<GenericNewznabSettings>
{
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public override string Name => "Generic Newznab";
public override string[] IndexerUrls => GetBaseUrlFromSettings();
public override string Description => "Newznab is an API search specification for Usenet";
public override bool FollowRedirect => true;
public override bool SupportsRedirect => true;
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities { get => GetCapabilitiesFromSettings(); protected set => base.Capabilities = value; }
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings, Definition).LimitsDefault.Value;
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new GenericNewznabRequestGenerator(_capabilitiesProvider)
{
PageSize = PageSize,
Settings = Settings
};
}
public override IParseIndexerResponse GetParser()
{
return new GenericNewznabRssParser(Settings.Categories);
}
public string[] GetBaseUrlFromSettings()
{
var baseUrl = "";
if (Definition == null || Settings == null || Settings.Categories == null)
{
return new string[] { baseUrl };
}
return new string[] { Settings.BaseUrl };
}
protected override GenericNewznabSettings GetDefaultBaseUrl(GenericNewznabSettings settings)
{
return settings;
}
public IndexerCapabilities GetCapabilitiesFromSettings()
{
var caps = new IndexerCapabilities();
if (Definition == null || Settings == null || Settings.Categories == null)
{
return caps;
}
foreach (var category in Settings.Categories)
{
caps.Categories.AddCategoryMapping(category.Name, category);
}
return caps;
}
public override IndexerCapabilities GetCapabilities()
{
// Newznab uses different Caps per site, so we need to cache them to db on first indexer add to prevent issues with loading UI and pulling caps every time.
return _capabilitiesProvider.GetCapabilities(Settings, Definition);
}
public override IEnumerable<ProviderDefinition> DefaultDefinitions
{
get
{
yield return GetDefinition("Generic Newznab", GetSettings(""));
}
}
public GenericNewznab(INewznabCapabilitiesProvider capabilitiesProvider, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
{
_capabilitiesProvider = capabilitiesProvider;
}
private IndexerDefinition GetDefinition(string name, GenericNewznabSettings settings)
{
return new IndexerDefinition
{
Enable = true,
Name = name,
Implementation = GetType().Name,
Settings = settings,
Protocol = DownloadProtocol.Usenet,
Privacy = IndexerPrivacy.Private,
SupportsRss = SupportsRss,
SupportsSearch = SupportsSearch,
SupportsRedirect = SupportsRedirect,
Capabilities = Capabilities
};
}
private GenericNewznabSettings GetSettings(string url, string apiPath = null)
{
var settings = new GenericNewznabSettings { BaseUrl = url };
if (apiPath.IsNotNullOrWhiteSpace())
{
settings.ApiPath = apiPath;
}
return settings;
}
protected override async Task Test(List<ValidationFailure> failures)
{
await base.Test(failures);
if (failures.HasErrors())
{
return;
}
failures.AddIfNotNull(TestCapabilities());
}
protected static List<int> CategoryIds(IndexerCapabilitiesCategories categories)
{
var l = categories.GetTorznabCategoryTree().Select(c => c.Id).ToList();
return l;
}
protected virtual ValidationFailure TestCapabilities()
{
try
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
if (capabilities.SearchParams != null && capabilities.SearchParams.Contains(SearchParam.Q))
{
return null;
}
if (capabilities.MovieSearchParams != null &&
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.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.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
{
return null;
}
if (capabilities.MusicSearchParams != null &&
new[] { MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album }.Any(v => capabilities.MusicSearchParams.Contains(v)))
{
return null;
}
if (capabilities.BookSearchParams != null &&
new[] { BookSearchParam.Q, BookSearchParam.Author, BookSearchParam.Title }.Any(v => capabilities.BookSearchParams.Contains(v)))
{
return null;
}
return new ValidationFailure(string.Empty, "This indexer does not support searching for tv, music, or movies :(. Tell your indexer staff to enable this or force add the indexer by disabling search, adding the indexer and then enabling it again.");
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to connect to indexer: " + ex.Message);
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
}
}
}
}

View File

@@ -0,0 +1,292 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using DryIoc;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers.Newznab
{
public class GenericNewznabRequestGenerator : IIndexerRequestGenerator
{
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public int MaxPages { get; set; }
public int PageSize { get; set; }
public GenericNewznabSettings Settings { get; set; }
public ProviderDefinition Definition { get; set; }
public GenericNewznabRequestGenerator(INewznabCapabilitiesProvider capabilitiesProvider)
{
_capabilitiesProvider = capabilitiesProvider;
MaxPages = 30;
PageSize = 100;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
if (searchCriteria.TmdbId.HasValue && capabilities.MovieSearchTmdbAvailable)
{
parameters.Add("tmdbid", searchCriteria.TmdbId.Value.ToString());
}
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && capabilities.MovieSearchImdbAvailable)
{
parameters.Add("imdbid", searchCriteria.ImdbId);
}
if (searchCriteria.TraktId.HasValue && capabilities.MovieSearchTraktAvailable)
{
parameters.Add("traktid", searchCriteria.TraktId.ToString());
}
//Workaround issue with Sphinx search returning garbage results on some indexers. If we don't use id parameters, fallback to t=search
if (parameters.Count == 0)
{
searchCriteria.SearchType = "search";
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
{
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
}
else
{
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.MovieSearchAvailable)
{
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
if (searchCriteria.Artist.IsNotNullOrWhiteSpace() && capabilities.MusicSearchArtistAvailable)
{
parameters.Add("artist", searchCriteria.Artist);
}
if (searchCriteria.Album.IsNotNullOrWhiteSpace() && capabilities.MusicSearchAlbumAvailable)
{
parameters.Add("album", searchCriteria.Album);
}
//Workaround issue with Sphinx search returning garbage results on some indexers. If we don't use id parameters, fallback to t=search
if (parameters.Count == 0)
{
searchCriteria.SearchType = "search";
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
{
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
}
else
{
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.MusicSearchAvailable)
{
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
if (searchCriteria.TvdbId.HasValue && capabilities.TvSearchTvdbAvailable)
{
parameters.Add("tvdbid", searchCriteria.TvdbId.Value.ToString());
}
if (searchCriteria.TmdbId.HasValue && capabilities.TvSearchTvdbAvailable)
{
parameters.Add("tmdbid", searchCriteria.TvdbId.Value.ToString());
}
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && capabilities.TvSearchImdbAvailable)
{
parameters.Add("imdbid", searchCriteria.ImdbId);
}
if (searchCriteria.TvMazeId.HasValue && capabilities.TvSearchTvMazeAvailable)
{
parameters.Add("tvmazeid", searchCriteria.TvMazeId.ToString());
}
if (searchCriteria.RId.HasValue && capabilities.TvSearchTvRageAvailable)
{
parameters.Add("rid", searchCriteria.RId.ToString());
}
if (searchCriteria.Season.HasValue && capabilities.TvSearchSeasonAvailable)
{
parameters.Add("season", NewznabifySeasonNumber(searchCriteria.Season.Value));
}
if (searchCriteria.Episode.IsNotNullOrWhiteSpace() && capabilities.TvSearchEpAvailable)
{
parameters.Add("ep", searchCriteria.Episode);
}
//Workaround issue with Sphinx search returning garbage results on some indexers. If we don't use id parameters, fallback to t=search
if (parameters.Count == 0)
{
searchCriteria.SearchType = "search";
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
{
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
}
else
{
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.TvSearchAvailable)
{
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
if (searchCriteria.Author.IsNotNullOrWhiteSpace() && capabilities.BookSearchAuthorAvailable)
{
parameters.Add("author", searchCriteria.Author);
}
if (searchCriteria.Title.IsNotNullOrWhiteSpace() && capabilities.BookSearchTitleAvailable)
{
parameters.Add("title", searchCriteria.Title);
}
//Workaround issue with Sphinx search returning garbage results on some indexers. If we don't use id parameters, fallback to t=search
if (parameters.Count == 0)
{
searchCriteria.SearchType = "search";
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
{
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
}
else
{
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.BookSearchAvailable)
{
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
{
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
}
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
{
var baseUrl = string.Format("{0}{1}?t={2}&extended=1", Settings.BaseUrl.TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchCriteria.SearchType);
var categories = searchCriteria.Categories;
if (categories != null && categories.Any())
{
var categoriesQuery = string.Join(",", categories.Distinct());
baseUrl += string.Format("&cat={0}", categoriesQuery);
}
if (Settings.AdditionalParameters.IsNotNullOrWhiteSpace())
{
baseUrl += Settings.AdditionalParameters;
}
if (Settings.ApiKey.IsNotNullOrWhiteSpace())
{
baseUrl += "&apikey=" + Settings.ApiKey;
}
if (searchCriteria.Limit.HasValue)
{
parameters.Add("limit", searchCriteria.Limit.ToString());
}
if (searchCriteria.Offset.HasValue)
{
parameters.Add("offset", searchCriteria.Offset.ToString());
}
var request = new IndexerRequest(string.Format("{0}&{1}", baseUrl, parameters.GetQueryString()), HttpAccept.Rss);
request.HttpRequest.AllowAutoRedirect = true;
yield return request;
}
private static string NewsnabifyTitle(string title)
{
return title.Replace("+", "%20");
}
// Temporary workaround for NNTMux considering season=0 -> null. '00' should work on existing newznab indexers.
private static string NewznabifySeasonNumber(int seasonNumber)
{
return seasonNumber == 0 ? "00" : seasonNumber.ToString();
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
}

View File

@@ -8,17 +8,17 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Newznab
{
public class NewznabRssParser : RssParser
public class GenericNewznabRssParser : RssParser
{
public const string ns = "{http://www.newznab.com/DTD/2010/feeds/attributes/}";
private readonly NewznabSettings _settings;
private readonly List<IndexerCategory> _categories;
public NewznabRssParser(NewznabSettings settings)
public GenericNewznabRssParser(List<IndexerCategory> categories)
{
PreferredEnclosureMimeTypes = UsenetEnclosureMimeTypes;
UseEnclosureUrl = true;
_settings = settings;
_categories = categories;
}
public static void CheckError(XDocument xdoc, IndexerResponse indexerResponse)
@@ -125,7 +125,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
if (int.TryParse(cat, out var intCategory))
{
var indexerCat = _settings.Categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
var indexerCat = _categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
if (indexerCat != null)
{

View File

@@ -0,0 +1,91 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Newznab
{
public class GenericNewznabSettingsValidator : AbstractValidator<GenericNewznabSettings>
{
private static readonly string[] ApiKeyWhiteList =
{
"nzbs.org",
"nzb.su",
"dognzb.cr",
"nzbplanet.net",
"nzbid.org",
"nzbndx.com",
"nzbindex.in"
};
private static bool ShouldHaveApiKey(GenericNewznabSettings settings)
{
if (settings.BaseUrl == null)
{
return false;
}
return ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c));
}
private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled);
public GenericNewznabSettingsValidator()
{
RuleFor(c => c.BaseUrl).ValidRootUrl();
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
RuleFor(c => c.AdditionalParameters).Matches(AdditionalParametersRegex)
.When(c => !c.AdditionalParameters.IsNullOrWhiteSpace());
RuleFor(c => c.VipExpiration).Must(c => c.IsValidDate())
.When(c => c.VipExpiration.IsNotNullOrWhiteSpace())
.WithMessage("Correctly formatted date is required");
RuleFor(c => c.VipExpiration).Must(c => c.IsFutureDate())
.When(c => c.VipExpiration.IsNotNullOrWhiteSpace())
.WithMessage("Must be a future date");
}
}
public class GenericNewznabSettings : IIndexerSettings
{
private static readonly GenericNewznabSettingsValidator Validator = new GenericNewznabSettingsValidator();
public GenericNewznabSettings()
{
ApiPath = "/api";
VipExpiration = "";
}
[FieldDefinition(0, Label = "URL")]
public string BaseUrl { get; set; }
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
public string ApiPath { get; set; }
[FieldDefinition(2, Label = "API Key", HelpText = "Site API Key", Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
public string AdditionalParameters { get; set; }
[FieldDefinition(6, Label = "VIP Expiration", HelpText = "Enter date (yyyy-mm-dd) for VIP Expiration or blank, Prowlarr will notify 1 week from expiration of VIP")]
public string VipExpiration { get; set; }
[FieldDefinition(7)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public List<IndexerCategory> Categories { get; set; }
// Field 8 is used by TorznabSettings MinimumSeeders
// If you need to add another field here, update TorznabSettings as well and this comment
public virtual NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -5,9 +5,10 @@ using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -16,10 +17,10 @@ namespace NzbDrone.Core.Indexers.Newznab
{
public class Newznab : UsenetIndexerBase<NewznabSettings>
{
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
private readonly IIndexerDefinitionUpdateService _definitionService;
public override string Name => "Newznab";
public override string[] IndexerUrls => GetBaseUrlFromSettings();
public override string[] IndexerUrls => new string[] { "" };
public override string Description => "Newznab is an API search specification for Usenet";
public override bool FollowRedirect => true;
public override bool SupportsRedirect => true;
@@ -27,130 +28,72 @@ namespace NzbDrone.Core.Indexers.Newznab
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities { get => GetCapabilitiesFromSettings(); protected set => base.Capabilities = value; }
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings, Definition).LimitsDefault.Value;
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new NewznabRequestGenerator(_capabilitiesProvider)
var defFile = _definitionService.GetCachedDefinition(Settings.DefinitionFile);
return new NewznabRequestGenerator()
{
PageSize = PageSize,
Settings = Settings
Settings = Settings,
Definition = defFile
};
}
public override IParseIndexerResponse GetParser()
{
return new NewznabRssParser(Settings);
}
var defFile = _definitionService.GetCachedDefinition(Settings.DefinitionFile);
var capabilities = new IndexerCapabilities();
capabilities.ParseYmlSearchModes(defFile.Caps.Modes);
capabilities.SupportsRawSearch = defFile.Caps.Allowrawsearch;
capabilities.MapYmlCategories(defFile);
public string[] GetBaseUrlFromSettings()
{
var baseUrl = "";
if (Definition == null || Settings == null || Settings.Categories == null)
{
return new string[] { baseUrl };
}
return new string[] { Settings.BaseUrl };
}
protected override NewznabSettings GetDefaultBaseUrl(NewznabSettings settings)
{
return settings;
}
public IndexerCapabilities GetCapabilitiesFromSettings()
{
var caps = new IndexerCapabilities();
if (Definition == null || Settings == null || Settings.Categories == null)
{
return caps;
}
foreach (var category in Settings.Categories)
{
caps.Categories.AddCategoryMapping(category.Name, category);
}
return caps;
}
public override IndexerCapabilities GetCapabilities()
{
// Newznab uses different Caps per site, so we need to cache them to db on first indexer add to prevent issues with loading UI and pulling caps every time.
return _capabilitiesProvider.GetCapabilities(Settings, Definition);
return new GenericNewznabRssParser(capabilities.Categories.GetTorznabCategoryList());
}
public override IEnumerable<ProviderDefinition> DefaultDefinitions
{
get
{
yield return GetDefinition("abNZB", GetSettings("https://abnzb.com"));
yield return GetDefinition("altHUB", GetSettings("https://api.althub.co.za"));
yield return GetDefinition("AnimeTosho (Usenet)", GetSettings("https://feed.animetosho.org"));
yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr"));
yield return GetDefinition("DrunkenSlug", GetSettings("https://drunkenslug.com"));
yield return GetDefinition("GingaDADDY", GetSettings("https://www.gingadaddy.com"));
yield return GetDefinition("Miatrix", GetSettings("https://www.miatrix.com"));
yield return GetDefinition("Newz-Complex", GetSettings("https://newz-complex.org/www"));
yield return GetDefinition("Newz69", GetSettings("https://newz69.keagaming.com"));
yield return GetDefinition("NinjaCentral", GetSettings("https://ninjacentral.co.za"));
yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su"));
yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat"));
yield return GetDefinition("NZBFinder", GetSettings("https://nzbfinder.ws"));
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
yield return GetDefinition("NzbNoob", GetSettings("https://www.nzbnoob.com"));
yield return GetDefinition("NZBNDX", GetSettings("https://www.nzbndx.com"));
yield return GetDefinition("NzbPlanet", GetSettings("https://api.nzbplanet.net"));
yield return GetDefinition("NZBStars", GetSettings("https://nzbstars.com"));
yield return GetDefinition("OZnzb", GetSettings("https://api.oznzb.com"));
yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com"));
yield return GetDefinition("SpotNZB", GetSettings("https://spotnzb.xyz"));
yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"));
yield return GetDefinition("Usenet Crawler", GetSettings("https://www.usenet-crawler.com"));
yield return GetDefinition("Generic Newznab", GetSettings(""));
foreach (var def in _definitionService.AllForImplementation(GetType().Name))
{
yield return GetDefinition(def);
}
}
}
public Newznab(INewznabCapabilitiesProvider capabilitiesProvider, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
public Newznab(IIndexerDefinitionUpdateService definitionService, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
{
_capabilitiesProvider = capabilitiesProvider;
_definitionService = definitionService;
}
private IndexerDefinition GetDefinition(string name, NewznabSettings settings)
private IndexerDefinition GetDefinition(IndexerMetaDefinition definition)
{
return new IndexerDefinition
{
Enable = true,
Name = name,
Name = definition.Name,
Language = definition.Language,
Description = definition.Description,
Implementation = GetType().Name,
Settings = settings,
IndexerUrls = definition.Links.ToArray(),
LegacyUrls = definition.Legacylinks.ToArray(),
Settings = new NewznabSettings { DefinitionFile = definition.File },
Protocol = DownloadProtocol.Usenet,
Privacy = IndexerPrivacy.Private,
Privacy = definition.Type switch
{
"private" => IndexerPrivacy.Private,
"public" => IndexerPrivacy.Public,
_ => IndexerPrivacy.SemiPrivate
},
SupportsRss = SupportsRss,
SupportsSearch = SupportsSearch,
SupportsRedirect = SupportsRedirect,
Capabilities = Capabilities
Capabilities = new IndexerCapabilities()
};
}
private NewznabSettings GetSettings(string url, string apiPath = null)
{
var settings = new NewznabSettings { BaseUrl = url };
if (apiPath.IsNotNullOrWhiteSpace())
{
settings.ApiPath = apiPath;
}
return settings;
}
protected override async Task Test(List<ValidationFailure> failures)
{
await base.Test(failures);
@@ -158,61 +101,21 @@ namespace NzbDrone.Core.Indexers.Newznab
{
return;
}
failures.AddIfNotNull(TestCapabilities());
}
protected static List<int> CategoryIds(IndexerCapabilitiesCategories categories)
public override object RequestAction(string action, IDictionary<string, string> query)
{
var l = categories.GetTorznabCategoryTree().Select(c => c.Id).ToList();
return l;
}
protected virtual ValidationFailure TestCapabilities()
{
try
if (action == "getUrls")
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var devices = ((IndexerDefinition)Definition).IndexerUrls;
if (capabilities.SearchParams != null && capabilities.SearchParams.Contains(SearchParam.Q))
return new
{
return null;
}
if (capabilities.MovieSearchParams != null &&
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.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.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
{
return null;
}
if (capabilities.MusicSearchParams != null &&
new[] { MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album }.Any(v => capabilities.MusicSearchParams.Contains(v)))
{
return null;
}
if (capabilities.BookSearchParams != null &&
new[] { BookSearchParam.Q, BookSearchParam.Author, BookSearchParam.Title }.Any(v => capabilities.BookSearchParams.Contains(v)))
{
return null;
}
return new ValidationFailure(string.Empty, "This indexer does not support searching for tv, music, or movies :(. Tell your indexer staff to enable this or force add the indexer by disabling search, adding the indexer and then enabling it again.");
options = devices.Select(d => new { Value = d, Name = d })
};
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to connect to indexer: " + ex.Message);
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
}
return null;
}
}
}

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
public interface INewznabCapabilitiesProvider
{
IndexerCapabilities GetCapabilities(NewznabSettings settings, ProviderDefinition definition);
IndexerCapabilities GetCapabilities(GenericNewznabSettings settings, ProviderDefinition definition);
}
public class NewznabCapabilitiesProvider : INewznabCapabilitiesProvider
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Indexers.Newznab
_logger = logger;
}
public IndexerCapabilities GetCapabilities(NewznabSettings indexerSettings, ProviderDefinition definition)
public IndexerCapabilities GetCapabilities(GenericNewznabSettings indexerSettings, ProviderDefinition definition)
{
var key = indexerSettings.ToJson();
var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings, definition), TimeSpan.FromDays(7));
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers.Newznab
return capabilities;
}
private IndexerCapabilities FetchCapabilities(NewznabSettings indexerSettings, ProviderDefinition definition)
private IndexerCapabilities FetchCapabilities(GenericNewznabSettings indexerSettings, ProviderDefinition definition)
{
var capabilities = new IndexerCapabilities();
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Indexers.Newznab
throw new XmlException("Invalid XML").WithData(response);
}
NewznabRssParser.CheckError(xDoc, new IndexerResponse(new IndexerRequest(response.Request), response));
GenericNewznabRssParser.CheckError(xDoc, new IndexerResponse(new IndexerRequest(response.Request), response));
var xmlRoot = xDoc.Element("caps");

View File

@@ -5,6 +5,7 @@ using System.Linq;
using DryIoc;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser;
using NzbDrone.Core.ThingiProvider;
@@ -13,23 +14,20 @@ namespace NzbDrone.Core.Indexers.Newznab
{
public class NewznabRequestGenerator : IIndexerRequestGenerator
{
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public int MaxPages { get; set; }
public int PageSize { get; set; }
public NewznabSettings Settings { get; set; }
public ProviderDefinition Definition { get; set; }
public CardigannDefinition Definition { get; set; }
public NewznabRequestGenerator(INewznabCapabilitiesProvider capabilitiesProvider)
public NewznabRequestGenerator()
{
_capabilitiesProvider = capabilitiesProvider;
MaxPages = 30;
PageSize = 100;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var capabilities = GetCapabilities();
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -67,15 +65,14 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var capabilities = GetCapabilities();
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -108,15 +105,14 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var capabilities = GetCapabilities();
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -174,15 +170,14 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var capabilities = GetCapabilities();
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -215,15 +210,15 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria,
parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var capabilities = GetCapabilities();
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -233,15 +228,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 baseUrl = string.Format("{0}{1}?t={2}&extended=1", ResolveSiteLink().TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchCriteria.SearchType);
var categories = capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
if (categories != null && categories.Any())
{
@@ -286,6 +281,34 @@ namespace NzbDrone.Core.Indexers.Newznab
return seasonNumber == 0 ? "00" : seasonNumber.ToString();
}
protected string ResolveSiteLink()
{
var settingsBaseUrl = Settings?.BaseUrl;
var defaultLink = Definition.Links.First();
if (settingsBaseUrl == null)
{
return defaultLink;
}
if (Definition?.Legacylinks?.Contains(settingsBaseUrl) ?? false)
{
return defaultLink;
}
return settingsBaseUrl;
}
private IndexerCapabilities GetCapabilities()
{
var capabilities = new IndexerCapabilities();
capabilities.ParseYmlSearchModes(Definition.Caps.Modes);
capabilities.MapYmlCategories(Definition);
return capabilities;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}

View File

@@ -4,40 +4,18 @@ using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Newznab
{
public class NewznabSettingsValidator : AbstractValidator<NewznabSettings>
{
private static readonly string[] ApiKeyWhiteList =
{
"nzbs.org",
"nzb.su",
"dognzb.cr",
"nzbplanet.net",
"nzbid.org",
"nzbndx.com",
"nzbindex.in"
};
private static bool ShouldHaveApiKey(NewznabSettings settings)
{
if (settings.BaseUrl == null)
{
return false;
}
return ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c));
}
private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled);
public NewznabSettingsValidator()
{
RuleFor(c => c.BaseUrl).ValidRootUrl();
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
RuleFor(c => c.AdditionalParameters).Matches(AdditionalParametersRegex)
.When(c => !c.AdditionalParameters.IsNullOrWhiteSpace());
@@ -51,7 +29,7 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
public class NewznabSettings : IIndexerSettings
public class NewznabSettings : IYmlIndexerSettings
{
private static readonly NewznabSettingsValidator Validator = new NewznabSettingsValidator();
@@ -61,7 +39,7 @@ namespace NzbDrone.Core.Indexers.Newznab
VipExpiration = "";
}
[FieldDefinition(0, Label = "URL")]
[FieldDefinition(0, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
@@ -76,6 +54,9 @@ namespace NzbDrone.Core.Indexers.Newznab
[FieldDefinition(6, Label = "VIP Expiration", HelpText = "Enter date (yyyy-mm-dd) for VIP Expiration or blank, Prowlarr will notify 1 week from expiration of VIP")]
public string VipExpiration { get; set; }
[FieldDefinition(0, Hidden = HiddenType.Hidden)]
public string DefinitionFile { get; set; }
[FieldDefinition(7)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();

View File

@@ -32,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Torznab
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new NewznabRequestGenerator(_capabilitiesProvider)
return new GenericNewznabRequestGenerator(_capabilitiesProvider)
{
PageSize = PageSize,
Settings = Settings

View File

@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Indexers.Torznab
}
}
public class TorznabSettings : NewznabSettings, ITorrentIndexerSettings
public class TorznabSettings : GenericNewznabSettings, ITorrentIndexerSettings
{
private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator();

View File

@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using DryIoc.ImTools;
using NzbDrone.Core.Indexers.Cardigann;
namespace NzbDrone.Core.Indexers
{
@@ -127,7 +129,7 @@ namespace NzbDrone.Core.Indexers
LimitsMax = 100;
}
public void ParseCardigannSearchModes(Dictionary<string, List<string>> modes)
public void ParseYmlSearchModes(Dictionary<string, List<string>> modes)
{
if (modes == null || !modes.Any())
{
@@ -169,6 +171,48 @@ namespace NzbDrone.Core.Indexers
}
}
public void MapYmlCategories(CardigannDefinition defFile)
{
if (defFile.Caps.Categories != null)
{
foreach (var category in defFile.Caps.Categories)
{
var cat = NewznabStandardCategory.GetCatByName(category.Value);
if (cat == null)
{
continue;
}
Categories.AddCategoryMapping(category.Key, cat);
}
}
if (defFile.Caps.Categorymappings != null)
{
foreach (var categorymapping in defFile.Caps.Categorymappings)
{
IndexerCategory torznabCat = null;
if (categorymapping.cat != null)
{
torznabCat = NewznabStandardCategory.GetCatByName(categorymapping.cat);
if (torznabCat == null)
{
continue;
}
}
Categories.AddCategoryMapping(categorymapping.id, torznabCat, categorymapping.desc);
//if (categorymapping.Default)
//{
// DefaultCategories.Add(categorymapping.id);
//}
}
}
}
public void ParseTvSearchParams(IEnumerable<string> paramsList)
{
if (paramsList == null)

View File

@@ -7,6 +7,7 @@ using NLog;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
@@ -50,11 +51,11 @@ namespace NzbDrone.Core.Indexers
foreach (var definition in definitions)
{
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
{
try
{
MapCardigannDefinition(definition);
MapYmlDefinition(definition);
}
catch
{
@@ -73,11 +74,11 @@ namespace NzbDrone.Core.Indexers
{
var definition = base.Get(id);
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
{
try
{
MapCardigannDefinition(definition);
MapYmlDefinition(definition);
}
catch
{
@@ -93,9 +94,9 @@ namespace NzbDrone.Core.Indexers
return base.Active().Where(c => c.Enable).ToList();
}
private void MapCardigannDefinition(IndexerDefinition definition)
private void MapYmlDefinition(IndexerDefinition definition)
{
var settings = (CardigannSettings)definition.Settings;
var settings = (IYmlIndexerSettings)definition.Settings;
var defFile = _definitionService.GetCachedDefinition(settings.DefinitionFile);
definition.ExtraFields = defFile.Settings;
@@ -121,51 +122,9 @@ namespace NzbDrone.Core.Indexers
_ => IndexerPrivacy.SemiPrivate
};
definition.Capabilities = new IndexerCapabilities();
definition.Capabilities.ParseCardigannSearchModes(defFile.Caps.Modes);
definition.Capabilities.ParseYmlSearchModes(defFile.Caps.Modes);
definition.Capabilities.SupportsRawSearch = defFile.Caps.Allowrawsearch;
MapCardigannCategories(definition, defFile);
}
private void MapCardigannCategories(IndexerDefinition def, CardigannDefinition defFile)
{
if (defFile.Caps.Categories != null)
{
foreach (var category in defFile.Caps.Categories)
{
var cat = NewznabStandardCategory.GetCatByName(category.Value);
if (cat == null)
{
continue;
}
def.Capabilities.Categories.AddCategoryMapping(category.Key, cat);
}
}
if (defFile.Caps.Categorymappings != null)
{
foreach (var categorymapping in defFile.Caps.Categorymappings)
{
IndexerCategory torznabCat = null;
if (categorymapping.cat != null)
{
torznabCat = NewznabStandardCategory.GetCatByName(categorymapping.cat);
if (torznabCat == null)
{
continue;
}
}
def.Capabilities.Categories.AddCategoryMapping(categorymapping.id, torznabCat, categorymapping.desc);
//if (categorymapping.Default)
//{
// DefaultCategories.Add(categorymapping.id);
//}
}
}
definition.Capabilities.MapYmlCategories(defFile);
}
public override IEnumerable<IndexerDefinition> GetDefaultDefinitions()
@@ -178,7 +137,7 @@ namespace NzbDrone.Core.Indexers
}
var definitions = provider.DefaultDefinitions
.Where(v => v.Name != null && (v.Name != typeof(Cardigann.Cardigann).Name || v.Name != typeof(Newznab.Newznab).Name || v.Name != typeof(Torznab.Torznab).Name));
.Where(v => v.Name != null && (v.Name != typeof(Cardigann.Cardigann).Name || v.Name != typeof(Newznab.Newznab).Name || v.Name != typeof(Newznab.GenericNewznab).Name || v.Name != typeof(Torznab.Torznab).Name));
foreach (IndexerDefinition definition in definitions)
{
@@ -203,7 +162,7 @@ namespace NzbDrone.Core.Indexers
definition.SupportsRedirect = provider.SupportsRedirect;
//We want to use the definition Caps and Privacy for Cardigann instead of the provider.
if (definition.Implementation != typeof(Cardigann.Cardigann).Name)
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) == null)
{
definition.IndexerUrls = provider.IndexerUrls;
definition.LegacyUrls = provider.LegacyUrls;
@@ -288,15 +247,15 @@ namespace NzbDrone.Core.Indexers
SetProviderCharacteristics(provider, definition);
if (definition.Implementation == typeof(Newznab.Newznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name)
if (definition.Implementation == typeof(Newznab.GenericNewznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name)
{
var settings = (NewznabSettings)definition.Settings;
var settings = (GenericNewznabSettings)definition.Settings;
settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList() ?? null;
}
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
{
MapCardigannDefinition(definition);
MapYmlDefinition(definition);
}
return base.Create(definition);
@@ -310,13 +269,13 @@ namespace NzbDrone.Core.Indexers
if (definition.Enable && (definition.Implementation == typeof(Newznab.Newznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name))
{
var settings = (NewznabSettings)definition.Settings;
var settings = (GenericNewznabSettings)definition.Settings;
settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList() ?? null;
}
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
{
MapCardigannDefinition(definition);
MapYmlDefinition(definition);
}
base.Update(definition);

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NzbDrone.Core.Indexers.Settings
{
public interface IYmlIndexerSettings : IIndexerSettings
{
public string DefinitionFile { get; set; }
}
}

View File

@@ -141,7 +141,7 @@
"BackupRetentionHelpText": "Automatische Backups, die älter als die Aufbewahrungsfrist sind, werden automatisch gelöscht",
"Backups": "Backups",
"BindAddress": "Adresse binden",
"BindAddressHelpText": "Gültige IPv4 Adresse oder \"*\" für alle Netzwerke",
"BindAddressHelpText": "Gültige IP-Adresse, \"localhost\" oder \"*\" für alle Netzwerke",
"Branch": "Git-Branch",
"BypassProxyForLocalAddresses": "Proxy für lokale Adressen umgehen",
"CertificateValidation": "Zertifikat Validierung",
@@ -435,7 +435,7 @@
"InstanceName": "Instanzname",
"InstanceNameHelpText": "Instanzname im Browser-Tab und für Syslog-Anwendungsname",
"SyncProfiles": "Sync-Profile",
"ThemeHelpText": "Prowlarr UI Theme ändern, inspiriert von {0}",
"ThemeHelpText": "Ändere das UI-Theme der Anwendung. Das 'Auto'-Theme verwendet dein Betriebssystem-Theme, um den hellen oder dunklen Modus einzustellen. Inspiriert von {0}",
"Duration": "Dauer",
"EditSyncProfile": "Synchronisationsprofil bearbeiten",
"ElapsedTime": "Vergangene Zeit",

View File

@@ -39,6 +39,7 @@
"AppProfileSelectHelpText": "App profiles are used to control RSS, Automatic Search and Interactive Search settings on application sync",
"Apps": "Apps",
"AppSettingsSummary": "Applications and settings to configure how Prowlarr interacts with your PVR programs",
"AreYouSureYouWantToDeleteCategory": "Are you sure you want to delete mapped category?",
"AreYouSureYouWantToResetYourAPIKey": "Are you sure you want to reset your API Key?",
"AudioSearch": "Audio Search",
"Auth": "Auth",
@@ -97,6 +98,7 @@
"DeleteAppProfile": "Delete App Profile",
"DeleteBackup": "Delete Backup",
"DeleteBackupMessageText": "Are you sure you want to delete the backup '{0}'?",
"DeleteClientCategory": "Delete Download Client Category",
"DeleteDownloadClient": "Delete Download Client",
"DeleteDownloadClientMessageText": "Are you sure you want to delete the download client '{0}'?",
"DeleteIndexerProxy": "Delete Indexer Proxy",
@@ -113,6 +115,7 @@
"Docker": "Docker",
"Donations": "Donations",
"DownloadClient": "Download Client",
"DownloadClientCategory": "Download Client Category",
"DownloadClients": "Download Clients",
"DownloadClientSettings": "Download Client Settings",
"DownloadClientsSettingsSummary": "Download clients configuration for integration into Prowlarr UI search",
@@ -226,6 +229,7 @@
"Logs": "Logs",
"MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details",
"Manual": "Manual",
"MappedCategories": "Mapped Categories",
"MappedDrivesRunningAsService": "Mapped network drives are not available when running as a Windows Service. Please see the FAQ for more information",
"MassEditor": "Mass Editor",
"Mechanism": "Mechanism",

View File

@@ -74,7 +74,7 @@
"ApplyTags": "Tunnistetoimenpide",
"Authentication": "Todennus",
"AuthenticationMethodHelpText": "Vaadi käyttäjätunnus ja salasana.",
"BindAddressHelpText": "Toimiva IPv4-osoite tai '*' (tähti) kaikille yhteyksille.",
"BindAddressHelpText": "Toimiva IP-osoite, localhost tai '*' (tähti) kaikille yhteyksille.",
"Close": "Sulje",
"DeleteNotification": "Poista kytkentä",
"Docker": "Docker",
@@ -100,7 +100,7 @@
"Protocol": "Protokolla",
"ProxyCheckBadRequestMessage": "Välityspalvelintesti epäonnistui. Tilakoodi: {0}",
"ProxyCheckFailedToTestMessage": "Välityspalvelintesti epäonnistui: {0}",
"ProxyCheckResolveIpMessage": "Määritetyn välityspalvelimen '{0}' IP-osoitteen selvitys epäonnistui",
"ProxyCheckResolveIpMessage": "Määritetyn välityspalvelimen '{0}' IP-osoitteen selvitys epäonnistui.",
"ProxyPasswordHelpText": "Käyttäjätunnus ja salasana tulee syöttää vain tarvittaessa. Muussa tapauksessa jätä kentät tyhjiksi.",
"ProxyType": "Välityspalvelimen tyyppi",
"ProxyUsernameHelpText": "Käyttäjätunnus ja salasana tulee syöttää vain tarvittaessa. Muussa tapauksessa jätä kentät tyhjiksi.",
@@ -121,7 +121,7 @@
"Security": "Suojaus",
"SuggestTranslationChange": "Ehdota käännösmuutosta",
"System": "Järjestelmä",
"SystemTimeCheckMessage": "Järjestelmän aika on heittä yli vuorokauden verran. Ajoitetut tehtävät eivät luultavasti toimi oikein ennen ajan korjausta.",
"SystemTimeCheckMessage": "Järjestelmän aika on pielessä yli vuorokauden. Ajoitetut tehtävät eivät luultavasti toimi oikein ennen sen korjausta.",
"TagCannotBeDeletedWhileInUse": "Tunnistetta ei voi poistaa, koska se on käytössä",
"TagIsNotUsedAndCanBeDeleted": "Tunnistetta ei ole määritetty millekään kohteelle, joten sen voi poistaa.",
"TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa sellaiset tunnisteet, joita ei ole määritetty millekään kohteelle.",
@@ -333,7 +333,7 @@
"ProwlarrSupportsAnyIndexer": "Prowlarr tukee Newznab- ja Torznab-yhteensopivien tietolähteiden ohella myös useita muita lähteitä vaihtoehdoilla \"Yleinen Newznab\" (Usenetille) ja 'Yleinen Torznab' (torrenteille).",
"SettingsIndexerLogging": "Tehostettu tietolähteiden valvonta",
"AddIndexerProxy": "Lisää tiedonhaun välityspalvelin",
"UISettingsSummary": "Päiväyksen ja kielen sekä heikentyneen värinäön asetukset",
"UISettingsSummary": "Kalenterin, päiväyksen ja kellonajan sekä kielen ja heikentyneelle värinäölle sopivan tilan asetukset.",
"SettingsIndexerLoggingHelpText": "Kirjaa tarkempia tietoja tietolähteiden toiminnasta, mukaanlukien vastaukset",
"IndexerTagsHelpText": "Tunnisteiden avulla voit määrittää tiedonhaun välityspalvelimet, mihin sovelluksiin tietolähteet synkronoidaan tai yksikertaisesti järjestellä tietolähteitäsi.",
"UnableToLoadAppProfiles": "Sovellusprofiilien lataus epäonnistui",
@@ -449,7 +449,7 @@
"EditSyncProfile": "Muokkaa synkronointiprofiilia",
"InstanceName": "Instanssin nimi",
"InstanceNameHelpText": "Instanssin nimi välilehdellä ja järjestelmälokissa",
"ThemeHelpText": "Vaihda Prowlarin käyttöliittymän teemaa, jota on inspiroinut {0}",
"ThemeHelpText": "Vaihda sovelluksen käyttöliittymän ulkoasua. \"Automaattinen\" vaihtaa vaalean ja tumman tilan käyttöjärjestelmäsi teemaa vastaavaksi. Innoittanut {0}.",
"Duration": "Kesto",
"ElapsedTime": "Kulunut aika",
"EnabledRedirected": "Kulunut, uudelleenohjattu",
@@ -461,6 +461,6 @@
"Parameters": "Parametrit",
"Queued": "Jonossa",
"Started": "Alkoi",
"ApplicationLongTermStatusCheckAllClientMessage": "Mikään tietolähde ei ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi.",
"ApplicationLongTermStatusCheckSingleClientMessage": "Tietolähteet eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi: {0}"
"ApplicationLongTermStatusCheckAllClientMessage": "Sovellukset eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi.",
"ApplicationLongTermStatusCheckSingleClientMessage": "Sovellukset eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi: {0}"
}

View File

@@ -54,7 +54,7 @@
"BranchUpdateMechanism": "A külső frissítési mechanizmus által használt ágazat",
"BranchUpdate": "Ágazattípus a Prowlarr frissítéseihez",
"Branch": "Ágazat",
"BindAddressHelpText": "Érvényes IP4-cím, vagy „*” minden interfészhez",
"BindAddressHelpText": "Érvényes IP-cím, localhost, vagy „*” minden interfészhez",
"BindAddress": "Kapcsolási Cím",
"BeforeUpdate": "Alkalmazásfrissítés előtt",
"Backups": "Biztonsági Mentés",
@@ -449,7 +449,7 @@
"MinimumSeedersHelpText": "Az alkalmazás által megkövetelt minimális Seeder az indexer számára",
"InstanceName": "Példány Neve",
"InstanceNameHelpText": "Példánynév a böngésző lapon és a syslog alkalmazás neve",
"ThemeHelpText": "Prowlarr felhasználói felület témájának módosítása, ihlette {0}",
"ThemeHelpText": "Változtasd meg az alkalmazás felhasználói felület témáját. Az „Auto” téma az operációs rendszer témáját használja a Világos vagy Sötét mód beállításához. Ihlette: {0}",
"Duration": "Időtartam",
"ElapsedTime": "Eltelt idő",
"EnabledRedirected": "Engedélyezve, átirányítva",

View File

@@ -93,7 +93,7 @@
"Cancel": "Annulla",
"BypassProxyForLocalAddresses": "Evita il Proxy per gli Indirizzi Locali",
"Branch": "Ramo",
"BindAddressHelpText": "Indirizzo IPv4 valido o '*' per tutte le interfacce",
"BindAddressHelpText": "Indirizzo IP valido, localhost o '*' per tutte le interfacce",
"BindAddress": "Indirizzo di Bind",
"Backups": "I Backup",
"BackupRetentionHelpText": "I backup automatici più vecchi del periodo di conservazione verranno eliminati automaticamente",
@@ -153,7 +153,7 @@
"BranchUpdateMechanism": "Ramo utilizzato dal sistema di aggiornamento esterno",
"BranchUpdate": "Ramo da usare per aggiornare Prowlarr",
"ApplyTagsHelpTexts2": "Aggiungi: Aggiunge le etichette alla lista esistente di etichette",
"ApplyTagsHelpTexts1": "Come applicare etichette agli Indicizzatori selezionati",
"ApplyTagsHelpTexts1": "Come applicare etichette agli indicizzatori selezionati",
"AddingTag": "Aggiungi etichetta",
"Password": "Password",
"OnHealthIssueHelpText": "Quando c'è un problema",
@@ -441,7 +441,7 @@
"SettingsSqlLoggingHelpText": "Scrivi a log tutte le query SQL di Prowlarr",
"SyncLevelAddRemove": "Solo aggiunte e rimozioni: Quando gli indicizzatori vengono aggiunti o rimossi da Prowlarr, verrà aggiornata questa applicazione remota.",
"SyncLevelFull": "Sincronizzazione completa: Manterrà gli indicizzatori di questa app completamente sincronizzati. Le modifiche apportate agli indicizzatori in Prowlarr sono poi sincronizzate con questa applicazione. Qualsiasi cambiamento fatto agli indicizzatori da remoto all'interno di questa applicazione sarà sovrascritto da Prowlarr alla prossima sincronizzazione.",
"MinimumSeedersHelpText": "Seeder minimi richiesti dall'Applicazione per far si che l'indicizzatore li prenda",
"MinimumSeedersHelpText": "Seeder minimi richiesti dall'Applicazione per far sì che l'indicizzatore li prenda",
"SyncProfile": "Profilo Sincronizzazione",
"SyncProfiles": "Profili Sincronizzazione",
"AddSyncProfile": "Aggiungi Profilo di Sincronizzazione",
@@ -449,7 +449,7 @@
"MinimumSeeders": "Seeder Minimi",
"InstanceName": "Nome Istanza",
"InstanceNameHelpText": "Nome dell'istanza nella scheda e per il nome dell'applicazione Syslog",
"ThemeHelpText": "Cambia il tema dell'interfaccia di Prowlarr, ispirato da {0}",
"ThemeHelpText": "Cambia il Tema dell'interfaccia dellapplicazione, il Tema 'Auto' userà il suo Tema di Sistema per impostare la modalità Chiara o Scura. Ispirato da {0}",
"LastDuration": "Ultima Durata",
"LastExecution": "Ultima esecuzione",
"Queued": "Messo in coda",
@@ -457,5 +457,10 @@
"ApplicationLongTermStatusCheckSingleClientMessage": "Alcuni Indicizzatori non sono disponibili da più di 6 ore a causa di errori: {0}",
"Duration": "Durata",
"Ended": "Finito",
"NextExecution": "Prossima esecuzione"
"NextExecution": "Prossima esecuzione",
"GrabTitle": "Ottieni il Titolo",
"Parameters": "Parametri",
"ElapsedTime": "Tempo trascorso",
"EnabledRedirected": "Abilitato, Reindirizzato",
"Started": "Iniziato"
}

View File

@@ -299,11 +299,11 @@
"MovieIndexScrollBottom": "Film Index: Scroll naar onder",
"IndexerLongTermStatusCheckSingleClientMessage": "Indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur: {0}",
"IndexerLongTermStatusCheckAllClientMessage": "Alle indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur",
"ClearHistoryMessageText": "Weet je zeker dat je alle geschiedenis van Prowlarr wilt verwijderen",
"ClearHistoryMessageText": "Weet je zeker dat je alle geschiedenis van Prowlarr wilt verwijderen?",
"ClearHistory": "Geschiedenis verwijderen",
"ApplicationStatusCheckSingleClientMessage": "Applicaties onbeschikbaar door fouten",
"ApplicationStatusCheckAllClientMessage": "Alle applicaties onbeschikbaar door fouten",
"AllIndexersHiddenDueToFilter": "Alle indexeerders zijn verborgen door actieve filter",
"AllIndexersHiddenDueToFilter": "Alle indexeerders zijn verborgen door actieve filter.",
"AddToDownloadClient": "Release toevoegen aan download client",
"AddNewIndexer": "Voeg nieuwe Indexeerder Toe",
"AddedToDownloadClient": "Release toegevoegd aan client",
@@ -417,5 +417,7 @@
"ApplicationLongTermStatusCheckSingleClientMessage": "Indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur: {0}",
"LastDuration": "Laatste Looptijd",
"LastExecution": "Laatste Uitvoering",
"Queued": "Afwachtend"
"Queued": "Afwachtend",
"Categories": "Categorieën",
"AddSyncProfile": "Synchronisatieprofiel toevoegen"
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
@@ -8,6 +9,8 @@ namespace Prowlarr.Api.V1.DownloadClient
public bool Enable { get; set; }
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; }
public List<DownloadClientCategory> Categories { get; set; }
public bool SupportsCategories { get; set; }
}
public class DownloadClientResourceMapper : ProviderResourceMapper<DownloadClientResource, DownloadClientDefinition>
@@ -24,6 +27,8 @@ namespace Prowlarr.Api.V1.DownloadClient
resource.Enable = definition.Enable;
resource.Protocol = definition.Protocol;
resource.Priority = definition.Priority;
resource.Categories = definition.Categories;
resource.SupportsCategories = definition.SupportsCategories;
return resource;
}
@@ -40,6 +45,7 @@ namespace Prowlarr.Api.V1.DownloadClient
definition.Enable = resource.Enable;
definition.Protocol = resource.Protocol;
definition.Priority = resource.Priority;
definition.Categories = resource.Categories;
return definition;
}

View File

@@ -4823,6 +4823,24 @@
},
"additionalProperties": false
},
"DownloadClientCategory": {
"type": "object",
"properties": {
"clientCategory": {
"type": "string",
"nullable": true
},
"categories": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
},
"nullable": true
}
},
"additionalProperties": false
},
"DownloadClientConfigResource": {
"type": "object",
"properties": {
@@ -4895,6 +4913,16 @@
"priority": {
"type": "integer",
"format": "int32"
},
"categories": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DownloadClientCategory"
},
"nullable": true
},
"supportsCategories": {
"type": "boolean"
}
},
"additionalProperties": false