mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
7 Commits
v0.4.10.21
...
cardigann-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c45eb68fa | ||
|
|
38ba810ae8 | ||
|
|
4e3f460a24 | ||
|
|
0d918a0aa9 | ||
|
|
a110412665 | ||
|
|
6c97f1b6ee | ||
|
|
470779ead2 |
@@ -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)'
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ function createMapStateToProps() {
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
value: value || [],
|
||||
values
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -0,0 +1,5 @@
|
||||
.deleteButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
171
frontend/src/Store/Actions/Settings/downloadClientCategories.js
Normal file
171
frontend/src/Store/Actions/Settings/downloadClientCategories.js
Normal 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: []
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -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')
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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("[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/NzbDrone.Core/Datastore/Migration/024_newznab_yml.cs
Normal file
14
src/NzbDrone.Core/Datastore/Migration/024_newznab_yml.cs
Normal 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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -38,5 +38,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
}
|
||||
|
||||
public override string Name => "Transmission";
|
||||
public override bool SupportsCategories => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -58,5 +58,6 @@ namespace NzbDrone.Core.Download.Clients.Vuze
|
||||
}
|
||||
|
||||
public override string Name => "Vuze";
|
||||
public override bool SupportsCategories => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
src/NzbDrone.Core/Download/DownloadClientCategory.cs
Normal file
14
src/NzbDrone.Core/Download/DownloadClientCategory.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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; }
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
195
src/NzbDrone.Core/Indexers/Definitions/Newznab/GenericNewznab.cs
Normal file
195
src/NzbDrone.Core/Indexers/Definitions/Newznab/GenericNewznab.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
13
src/NzbDrone.Core/Indexers/Settings/IYmlIndexerSettings.cs
Normal file
13
src/NzbDrone.Core/Indexers/Settings/IYmlIndexerSettings.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 dell’applicazione, 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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user