New: Per Indexer Proxies

Fixes #281
pull/425/head
Qstick 4 years ago
parent 31886e8d35
commit 7480ebea85

@ -11,6 +11,7 @@ import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSetti
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Settings from 'Settings/Settings';
import TagSettings from 'Settings/Tags/TagSettings';
@ -90,6 +91,11 @@ function AppRoutes(props) {
component={Settings}
/>
<Route
path="/settings/indexers"
component={IndexerSettings}
/>
<Route
path="/settings/applications"
component={ApplicationSettingsConnector}

@ -11,8 +11,6 @@ class InfoInput extends Component {
value
} = this.props;
console.log(this.props);
return (
<span dangerouslySetInnerHTML={{ __html: value }} />
);

@ -48,6 +48,10 @@ const links = [
title: translate('Settings'),
to: '/settings',
children: [
{
title: translate('Indexers'),
to: '/settings/indexers'
},
{
title: translate('Apps'),
to: '/settings/applications'

@ -45,6 +45,7 @@ function EditIndexerModalContent(props) {
supportsRss,
supportsRedirect,
appProfileId,
tags,
fields,
priority
} = item;
@ -151,6 +152,18 @@ function EditIndexerModalContent(props) {
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText="Use tags to specify default clients, specify Indexer Proxies, or just to organize your indexers."
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>

@ -59,7 +59,6 @@ class EditIndexerModalContentConnector extends Component {
}
onAdvancedSettingsPress = () => {
console.log('settings');
this.props.toggleAdvancedSettings();
}

@ -0,0 +1,44 @@
.indexerProxy {
composes: card from '~Components/Card.css';
position: relative;
width: 300px;
height: 100px;
}
.underlay {
@add-mixin cover;
}
.overlay {
@add-mixin linkOverlay;
padding: 10px;
}
.name {
text-align: center;
font-weight: lighter;
font-size: 24px;
}
.actions {
margin-top: 20px;
text-align: right;
}
.presetsMenu {
composes: menu from '~Components/Menu/Menu.css';
display: inline-block;
margin: 0 5px;
}
.presetsMenuButton {
composes: button from '~Components/Link/Button.css';
&::after {
margin-left: 5px;
content: '\25BE';
}
}

@ -0,0 +1,111 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerProxyPresetMenuItem from './AddIndexerProxyPresetMenuItem';
import styles from './AddIndexerProxyItem.css';
class AddIndexerProxyItem extends Component {
//
// Listeners
onIndexerProxySelect = () => {
const {
implementation
} = this.props;
this.props.onIndexerProxySelect({ implementation });
}
//
// Render
render() {
const {
implementation,
implementationName,
infoLink,
presets,
onIndexerProxySelect
} = this.props;
const hasPresets = !!presets && !!presets.length;
return (
<div
className={styles.indexerProxy}
>
<Link
className={styles.underlay}
onPress={this.onIndexerProxySelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onIndexerProxySelect}
>
Custom
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
Presets
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddIndexerProxyPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
onPress={onIndexerProxySelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
}
AddIndexerProxyItem.propTypes = {
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired,
presets: PropTypes.arrayOf(PropTypes.object),
onIndexerProxySelect: PropTypes.func.isRequired
};
export default AddIndexerProxyItem;

@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddIndexerProxyModalContentConnector from './AddIndexerProxyModalContentConnector';
function AddIndexerProxyModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddIndexerProxyModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddIndexerProxyModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerProxyModal;

@ -0,0 +1,5 @@
.indexerProxies {
display: flex;
justify-content: center;
flex-wrap: wrap;
}

@ -0,0 +1,88 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 translate from 'Utilities/String/translate';
import AddIndexerProxyItem from './AddIndexerProxyItem';
import styles from './AddIndexerProxyModalContent.css';
class AddIndexerProxyModalContent extends Component {
//
// Render
render() {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema,
onIndexerProxySelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Add IndexerProxy
</ModalHeader>
<ModalBody>
{
isSchemaFetching &&
<LoadingIndicator />
}
{
!isSchemaFetching && !!schemaError &&
<div>
{translate('UnableToAddANewIndexerProxyPleaseTryAgain')}
</div>
}
{
isSchemaPopulated && !schemaError &&
<div>
<div className={styles.indexerProxies}>
{
schema.map((indexerProxy) => {
return (
<AddIndexerProxyItem
key={indexerProxy.implementation}
implementation={indexerProxy.implementation}
{...indexerProxy}
onIndexerProxySelect={onIndexerProxySelect}
/>
);
})
}
</div>
</div>
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddIndexerProxyModalContent.propTypes = {
isSchemaFetching: PropTypes.bool.isRequired,
isSchemaPopulated: PropTypes.bool.isRequired,
schemaError: PropTypes.object,
schema: PropTypes.arrayOf(PropTypes.object).isRequired,
onIndexerProxySelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerProxyModalContent;

@ -0,0 +1,70 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerProxySchema, selectIndexerProxySchema } from 'Store/Actions/settingsActions';
import AddIndexerProxyModalContent from './AddIndexerProxyModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexerProxies,
(indexerProxies) => {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
} = indexerProxies;
return {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
};
}
);
}
const mapDispatchToProps = {
fetchIndexerProxySchema,
selectIndexerProxySchema
};
class AddIndexerProxyModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexerProxySchema();
}
//
// Listeners
onIndexerProxySelect = ({ implementation, name }) => {
this.props.selectIndexerProxySchema({ implementation, presetName: name });
this.props.onModalClose({ indexerProxySelected: true });
}
//
// Render
render() {
return (
<AddIndexerProxyModalContent
{...this.props}
onIndexerProxySelect={this.onIndexerProxySelect}
/>
);
}
}
AddIndexerProxyModalContentConnector.propTypes = {
fetchIndexerProxySchema: PropTypes.func.isRequired,
selectIndexerProxySchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerProxyModalContentConnector);

@ -0,0 +1,49 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddIndexerProxyPresetMenuItem extends Component {
//
// Listeners
onPress = () => {
const {
name,
implementation
} = this.props;
this.props.onPress({
name,
implementation
});
}
//
// Render
render() {
const {
name,
implementation,
...otherProps
} = this.props;
return (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddIndexerProxyPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddIndexerProxyPresetMenuItem;

@ -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 EditIndexerProxyModalContentConnector from './EditIndexerProxyModalContentConnector';
function EditIndexerProxyModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditIndexerProxyModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditIndexerProxyModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditIndexerProxyModal;

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

@ -0,0 +1,11 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}
.message {
composes: alert from '~Components/Alert.css';
margin-bottom: 30px;
}

@ -0,0 +1,175 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
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 Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 './EditIndexerProxyModalContent.css';
function EditIndexerProxyModalContent(props) {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onDeleteIndexerProxyPress,
...otherProps
} = props;
const {
id,
implementationName,
name,
tags,
fields,
message
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? 'Edit' : 'Add'} Proxy - ${implementationName}`}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToAddANewIndexerProxyPleaseTryAgain')}
</div>
}
{
!isFetching && !error &&
<Form {...otherProps}>
{
!!message &&
<Alert
className={styles.message}
kind={message.value.type}
>
{message.value.message}
</Alert>
}
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('TagsHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="indexerProxy"
providerData={item}
section="settings.indexerProxies"
{...field}
onChange={onFieldChange}
/>
);
})
}
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerProxyPress}
>
{translate('Delete')}
</Button>
}
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditIndexerProxyModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isTesting: PropTypes.bool.isRequired,
saveError: 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,
onDeleteIndexerProxyPress: PropTypes.func
};
export default EditIndexerProxyModalContent;

@ -0,0 +1,88 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveIndexerProxy, setIndexerProxyFieldValue, setIndexerProxyValue, testIndexerProxy } from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditIndexerProxyModalContent from './EditIndexerProxyModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('indexerProxies'),
(advancedSettings, indexerProxy) => {
return {
advancedSettings,
...indexerProxy
};
}
);
}
const mapDispatchToProps = {
setIndexerProxyValue,
setIndexerProxyFieldValue,
saveIndexerProxy,
testIndexerProxy
};
class EditIndexerProxyModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setIndexerProxyValue({ name, value });
}
onFieldChange = ({ name, value }) => {
this.props.setIndexerProxyFieldValue({ name, value });
}
onSavePress = () => {
this.props.saveIndexerProxy({ id: this.props.id });
}
onTestPress = () => {
this.props.testIndexerProxy({ id: this.props.id });
}
//
// Render
render() {
return (
<EditIndexerProxyModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditIndexerProxyModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setIndexerProxyValue: PropTypes.func.isRequired,
setIndexerProxyFieldValue: PropTypes.func.isRequired,
saveIndexerProxy: PropTypes.func.isRequired,
testIndexerProxy: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerProxyModalContentConnector);

@ -0,0 +1,20 @@
.indexerProxies {
display: flex;
flex-wrap: wrap;
}
.addIndexerProxy {
composes: indexerProxy from '~./IndexerProxy.css';
background-color: $cardAlternateBackgroundColor;
color: $gray;
text-align: center;
}
.center {
display: inline-block;
padding: 5px 20px 0;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
}

@ -0,0 +1,121 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerProxyModal from './AddIndexerProxyModal';
import EditIndexerProxyModalConnector from './EditIndexerProxyModalConnector';
import IndexerProxy from './IndexerProxy';
import styles from './IndexerProxies.css';
class IndexerProxies extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddIndexerProxyModalOpen: false,
isEditIndexerProxyModalOpen: false
};
}
//
// Listeners
onAddIndexerProxyPress = () => {
this.setState({ isAddIndexerProxyModalOpen: true });
}
onAddIndexerProxyModalClose = ({ indexerProxySelected = false } = {}) => {
this.setState({
isAddIndexerProxyModalOpen: false,
isEditIndexerProxyModalOpen: indexerProxySelected
});
}
onEditIndexerProxyModalClose = () => {
this.setState({ isEditIndexerProxyModalOpen: false });
}
//
// Render
render() {
const {
items,
tagList,
indexerList,
onConfirmDeleteIndexerProxy,
...otherProps
} = this.props;
const {
isAddIndexerProxyModalOpen,
isEditIndexerProxyModalOpen
} = this.state;
return (
<FieldSet legend={translate('Indexer Proxies')}>
<PageSectionContent
errorMessage={translate('UnableToLoadIndexerProxies')}
{...otherProps}
>
<div className={styles.indexerProxies}>
{
items.map((item) => {
return (
<IndexerProxy
key={item.id}
{...item}
tagList={tagList}
indexerList={indexerList}
onConfirmDeleteIndexerProxy={onConfirmDeleteIndexerProxy}
/>
);
})
}
<Card
className={styles.addIndexerProxy}
onPress={this.onAddIndexerProxyPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<AddIndexerProxyModal
isOpen={isAddIndexerProxyModalOpen}
onModalClose={this.onAddIndexerProxyModalClose}
/>
<EditIndexerProxyModalConnector
isOpen={isEditIndexerProxyModalOpen}
onModalClose={this.onEditIndexerProxyModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
IndexerProxies.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteIndexerProxy: PropTypes.func.isRequired
};
export default IndexerProxies;

@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName';
import IndexerProxies from './IndexerProxies';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.indexerProxies', sortByName),
createSortedSectionSelector('indexers', sortByName),
createTagsSelector(),
(indexerProxies, indexers, tagList) => {
return {
...indexerProxies,
indexerList: indexers.items,
tagList
};
}
);
}
const mapDispatchToProps = {
fetchIndexerProxies,
deleteIndexerProxy
};
class IndexerProxiesConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexerProxies();
}
//
// Listeners
onConfirmDeleteIndexerProxy = (id) => {
this.props.deleteIndexerProxy({ id });
}
//
// Render
render() {
return (
<IndexerProxies
{...this.props}
onConfirmDeleteIndexerProxy={this.onConfirmDeleteIndexerProxy}
/>
);
}
}
IndexerProxiesConnector.propTypes = {
fetchIndexerProxies: PropTypes.func.isRequired,
deleteIndexerProxy: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerProxiesConnector);

@ -0,0 +1,23 @@
.indexerProxy {
composes: card from '~Components/Card.css';
width: 290px;
}
.name {
@add-mixin truncate;
margin-bottom: 20px;
font-weight: 300;
font-size: 24px;
}
.indexers {
flex: 1 0 auto;
}
.enabled {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
}

@ -0,0 +1,144 @@
import _ from 'lodash';
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 TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditIndexerProxyModalConnector from './EditIndexerProxyModalConnector';
import styles from './IndexerProxy.css';
class IndexerProxy extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditIndexerProxyModalOpen: false,
isDeleteIndexerProxyModalOpen: false
};
}
//
// Listeners
onEditIndexerProxyPress = () => {
this.setState({ isEditIndexerProxyModalOpen: true });
}
onEditIndexerProxyModalClose = () => {
this.setState({ isEditIndexerProxyModalOpen: false });
}
onDeleteIndexerProxyPress = () => {
this.setState({
isEditIndexerProxyModalOpen: false,
isDeleteIndexerProxyModalOpen: true
});
}
onDeleteIndexerProxyModalClose= () => {
this.setState({ isDeleteIndexerProxyModalOpen: false });
}
onConfirmDeleteIndexerProxy = () => {
this.props.onConfirmDeleteIndexerProxy(this.props.id);
}
//
// Render
render() {
const {
id,
name,
tags,
tagList,
indexerList
} = this.props;
return (
<Card
className={styles.indexerProxy}
overlayContent={true}
onPress={this.onEditIndexerProxyPress}
>
<div className={styles.name}>
{name}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<div className={styles.indexers}>
{
tags.map((t) => {
const indexers = _.filter(indexerList, { tags: [t] });
if (!indexers || indexers.length === 0) {
return null;
}
return indexers.map((i) => {
return (
<Label
key={i.name}
kind={kinds.SUCCESS}
>
{i.name}
</Label>
);
});
})
}
</div>
{
!tags || tags.length === 0 ?
<Label
kind={kinds.DISABLED}
outline={true}
>
Disabled
</Label> :
null
}
<EditIndexerProxyModalConnector
id={id}
isOpen={this.state.isEditIndexerProxyModalOpen}
onModalClose={this.onEditIndexerProxyModalClose}
onDeleteIndexerProxyPress={this.onDeleteIndexerProxyPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteIndexerProxyModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexerProxy')}
message={translate('DeleteIndexerProxyMessageText', [name])}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexerProxy}
onCancel={this.onDeleteIndexerProxyModalClose}
/>
</Card>
);
}
}
IndexerProxy.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteIndexerProxy: PropTypes.func.isRequired
};
export default IndexerProxy;

@ -0,0 +1,22 @@
import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import IndexerProxiesConnector from './IndexerProxies/IndexerProxiesConnector';
function IndexerSettings() {
return (
<PageContent title={translate('Proxies')}>
<SettingsToolbarConnector
showSave={false}
/>
<PageContentBody>
<IndexerProxiesConnector />
</PageContentBody>
</PageContent>
);
}
export default IndexerSettings;

@ -1,47 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import titleCase from 'Utilities/String/titleCase';
function TagDetailsDelayProfile(props) {
const {
preferredProtocol,
enableUsenet,
enableTorrent,
usenetDelay,
torrentDelay
} = props;
return (
<div>
<div>
Protocol: {titleCase(preferredProtocol)}
</div>
<div>
{
enableUsenet ?
`Usenet Delay: ${usenetDelay}` :
'Usenet disabled'
}
</div>
<div>
{
enableTorrent ?
`Torrent Delay: ${torrentDelay}` :
'Torrents disabled'
}
</div>
</div>
);
}
TagDetailsDelayProfile.propTypes = {
preferredProtocol: PropTypes.string.isRequired,
enableUsenet: PropTypes.bool.isRequired,
enableTorrent: PropTypes.bool.isRequired,
usenetDelay: PropTypes.number.isRequired,
torrentDelay: PropTypes.number.isRequired
};
export default TagDetailsDelayProfile;

@ -1,26 +1,22 @@
import PropTypes from 'prop-types';
import React from 'react';
import FieldSet from 'Components/FieldSet';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
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 { kinds } from 'Helpers/Props';
import split from 'Utilities/String/split';
import translate from 'Utilities/String/translate';
import TagDetailsDelayProfile from './TagDetailsDelayProfile';
import styles from './TagDetailsModalContent.css';
function TagDetailsModalContent(props) {
const {
label,
isTagUsed,
movies,
delayProfiles,
indexers,
notifications,
restrictions,
indexerProxies,
onModalClose,
onDeleteTagPress
} = props;
@ -40,13 +36,13 @@ function TagDetailsModalContent(props) {
}
{
!!movies.length &&
<FieldSet legend={translate('Movies')}>
!!indexers.length &&
<FieldSet legend={translate('Indexers')}>
{
movies.map((item) => {
indexers.map((item) => {
return (
<div key={item.id}>
{item.title}
{item.name}
</div>
);
})
@ -54,35 +50,6 @@ function TagDetailsModalContent(props) {
</FieldSet>
}
{
!!delayProfiles.length &&
<FieldSet legend={translate('DelayProfile')}>
{
delayProfiles.map((item) => {
const {
id,
preferredProtocol,
enableUsenet,
enableTorrent,
usenetDelay,
torrentDelay
} = item;
return (
<TagDetailsDelayProfile
key={id}
preferredProtocol={preferredProtocol}
enableUsenet={enableUsenet}
enableTorrent={enableTorrent}
usenetDelay={usenetDelay}
torrentDelay={torrentDelay}
/>
);
})
}
</FieldSet>
}
{
!!notifications.length &&
<FieldSet legend={translate('Connections')}>
@ -99,44 +66,13 @@ function TagDetailsModalContent(props) {
}
{
!!restrictions.length &&
<FieldSet legend={translate('Restrictions')}>
!!indexerProxies.length &&
<FieldSet legend={translate('Indexer Proxies')}>
{
restrictions.map((item) => {
indexerProxies.map((item) => {
return (
<div
key={item.id}
className={styles.restriction}
>
<div>
{
split(item.required).map((r) => {
return (
<Label
key={r}
kind={kinds.SUCCESS}
>
{r}
</Label>
);
})
}
</div>
<div>
{
split(item.ignored).map((i) => {
return (
<Label
key={i}
kind={kinds.DANGER}
>
{i}
</Label>
);
})
}
</div>
<div key={item.id}>
{item.name}
</div>
);
})
@ -171,10 +107,9 @@ function TagDetailsModalContent(props) {
TagDetailsModalContent.propTypes = {
label: PropTypes.string.isRequired,
isTagUsed: PropTypes.bool.isRequired,
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
restrictions: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerProxies: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteTagPress: PropTypes.func.isRequired
};

@ -1,6 +1,5 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import TagDetailsModalContent from './TagDetailsModalContent';
function findMatchingItems(ids, items) {
@ -9,49 +8,39 @@ function findMatchingItems(ids, items) {
});
}
function createUnorderedMatchingMoviesSelector() {
function createMatchingIndexersSelector() {
return createSelector(
(state, { indexerIds }) => indexerIds,
createAllIndexersSelector(),
(state) => state.indexers.items,
findMatchingItems
);
}
function createMatchingMoviesSelector() {
function createMatchingIndexerProxiesSelector() {
return createSelector(
createUnorderedMatchingMoviesSelector(),
(movies) => {
return movies.sort((movieA, movieB) => {
const sortTitleA = movieA.sortTitle;
const sortTitleB = movieB.sortTitle;
if (sortTitleA > sortTitleB) {
return 1;
} else if (sortTitleA < sortTitleB) {
return -1;
}
return 0;
});
}
(state, { notificationIds }) => notificationIds,
(state) => state.settings.notifications.items,
findMatchingItems
);
}
function createMatchingNotificationsSelector() {
return createSelector(
(state, { notificationIds }) => notificationIds,
(state) => state.settings.notifications.items,
(state, { indexerProxyIds }) => indexerProxyIds,
(state) => state.settings.indexerProxies.items,
findMatchingItems
);
}
function createMapStateToProps() {
return createSelector(
createMatchingMoviesSelector(),
createMatchingIndexersSelector(),
createMatchingIndexerProxiesSelector(),
createMatchingNotificationsSelector(),
(movies, notifications) => {
(indexers, indexerProxies, notifications) => {
return {
movies,
indexers,
indexerProxies,
notifications
};
}

@ -53,11 +53,9 @@ class Tag extends Component {
render() {
const {
label,
delayProfileIds,
notificationIds,
restrictionIds,
importListIds,
movieIds
indexerIds,
indexerProxyIds
} = this.props;
const {
@ -66,11 +64,9 @@ class Tag extends Component {
} = this.state;
const isTagUsed = !!(
delayProfileIds.length ||
indexerIds.length ||
notificationIds.length ||
restrictionIds.length ||
importListIds.length ||
movieIds.length
indexerProxyIds.length
);
return (
@ -87,16 +83,9 @@ class Tag extends Component {
isTagUsed &&
<div>
{
!!movieIds.length &&
!!indexerIds.length &&
<div>
{movieIds.length} movies
</div>
}
{
!!delayProfileIds.length &&
<div>
{delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'}
{indexerIds.length} indexer{indexerIds.length > 1 && 's'}
</div>
}
@ -108,16 +97,9 @@ class Tag extends Component {
}
{
!!restrictionIds.length &&
<div>
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'}
</div>
}
{
!!importListIds.length &&
!!indexerProxyIds.length &&
<div>
{importListIds.length} list{importListIds.length > 1 && 's'}
{indexerProxyIds.length} indexerProxy{indexerProxyIds.length > 1 && 's'}
</div>
}
</div>
@ -133,11 +115,9 @@ class Tag extends Component {
<TagDetailsModal
label={label}
isTagUsed={isTagUsed}
movieIds={movieIds}
delayProfileIds={delayProfileIds}
indexerIds={indexerIds}
notificationIds={notificationIds}
restrictionIds={restrictionIds}
importListIds={importListIds}
indexerProxyIds={indexerProxyIds}
isOpen={isDetailsModalOpen}
onModalClose={this.onDetailsModalClose}
onDeleteTagPress={this.onDeleteTagPress}
@ -160,20 +140,16 @@ class Tag extends Component {
Tag.propTypes = {
id: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerProxyIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onConfirmDeleteTag: PropTypes.func.isRequired
};
Tag.defaultProps = {
delayProfileIds: [],
indexerIds: [],
notificationIds: [],
restrictionIds: [],
importListIds: [],
movieIds: []
indexerProxyIds: []
};
export default Tag;

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchNotifications } from 'Store/Actions/settingsActions';
import { fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
import { fetchTagDetails } from 'Store/Actions/tagActions';
import Tags from './Tags';
@ -26,7 +26,8 @@ function createMapStateToProps() {
const mapDispatchToProps = {
dispatchFetchTagDetails: fetchTagDetails,
dispatchFetchNotifications: fetchNotifications
dispatchFetchNotifications: fetchNotifications,
dispatchFetchIndexerProxies: fetchIndexerProxies
};
class MetadatasConnector extends Component {
@ -37,11 +38,13 @@ class MetadatasConnector extends Component {
componentDidMount() {
const {
dispatchFetchTagDetails,
dispatchFetchNotifications
dispatchFetchNotifications,
dispatchFetchIndexerProxies
} = this.props;
dispatchFetchTagDetails();
dispatchFetchNotifications();
dispatchFetchIndexerProxies();
}
//
@ -58,7 +61,8 @@ class MetadatasConnector extends Component {
MetadatasConnector.propTypes = {
dispatchFetchTagDetails: PropTypes.func.isRequired,
dispatchFetchNotifications: PropTypes.func.isRequired
dispatchFetchNotifications: PropTypes.func.isRequired,
dispatchFetchIndexerProxies: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);

@ -0,0 +1,110 @@
import { createAction } from 'redux-actions';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
//
// Variables
const section = 'settings.indexerProxies';
//
// Actions Types
export const FETCH_INDEXER_PROXYS = 'settings/indexerProxies/fetchIndexerProxies';
export const FETCH_INDEXER_PROXY_SCHEMA = 'settings/indexerProxies/fetchIndexerProxySchema';
export const SELECT_INDEXER_PROXY_SCHEMA = 'settings/indexerProxies/selectIndexerProxySchema';
export const SET_INDEXER_PROXY_VALUE = 'settings/indexerProxies/setIndexerProxyValue';
export const SET_INDEXER_PROXY_FIELD_VALUE = 'settings/indexerProxies/setIndexerProxyFieldValue';
export const SAVE_INDEXER_PROXY = 'settings/indexerProxies/saveIndexerProxy';
export const CANCEL_SAVE_INDEXER_PROXY = 'settings/indexerProxies/cancelSaveIndexerProxy';
export const DELETE_INDEXER_PROXY = 'settings/indexerProxies/deleteIndexerProxy';
export const TEST_INDEXER_PROXY = 'settings/indexerProxies/testIndexerProxy';
export const CANCEL_TEST_INDEXER_PROXY = 'settings/indexerProxies/cancelTestIndexerProxy';
//
// Action Creators
export const fetchIndexerProxies = createThunk(FETCH_INDEXER_PROXYS);
export const fetchIndexerProxySchema = createThunk(FETCH_INDEXER_PROXY_SCHEMA);
export const selectIndexerProxySchema = createAction(SELECT_INDEXER_PROXY_SCHEMA);
export const saveIndexerProxy = createThunk(SAVE_INDEXER_PROXY);
export const cancelSaveIndexerProxy = createThunk(CANCEL_SAVE_INDEXER_PROXY);
export const deleteIndexerProxy = createThunk(DELETE_INDEXER_PROXY);
export const testIndexerProxy = createThunk(TEST_INDEXER_PROXY);
export const cancelTestIndexerProxy = createThunk(CANCEL_TEST_INDEXER_PROXY);
export const setIndexerProxyValue = createAction(SET_INDEXER_PROXY_VALUE, (payload) => {
return {
section,
...payload
};
});
export const setIndexerProxyFieldValue = createAction(SET_INDEXER_PROXY_FIELD_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
isSchemaFetching: false,
isSchemaPopulated: false,
schemaError: null,
schema: [],
selectedSchema: {},
isSaving: false,
saveError: null,
isTesting: false,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_PROXYS]: createFetchHandler(section, '/indexerProxy'),
[FETCH_INDEXER_PROXY_SCHEMA]: createFetchSchemaHandler(section, '/indexerProxy/schema'),
[SAVE_INDEXER_PROXY]: createSaveProviderHandler(section, '/indexerProxy'),
[CANCEL_SAVE_INDEXER_PROXY]: createCancelSaveProviderHandler(section),
[DELETE_INDEXER_PROXY]: createRemoveItemHandler(section, '/indexerProxy'),
[TEST_INDEXER_PROXY]: createTestProviderHandler(section, '/indexerProxy'),
[CANCEL_TEST_INDEXER_PROXY]: createCancelTestProviderHandler(section)
},
//
// Reducers
reducers: {
[SET_INDEXER_PROXY_VALUE]: createSetSettingValueReducer(section),
[SET_INDEXER_PROXY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
[SELECT_INDEXER_PROXY_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
return selectedSchema;
});
}
}
};

@ -7,6 +7,7 @@ import development from './Settings/development';
import downloadClients from './Settings/downloadClients';
import general from './Settings/general';
import indexerCategories from './Settings/indexerCategories';
import indexerProxies from './Settings/indexerProxies';
import languages from './Settings/languages';
import notifications from './Settings/notifications';
import ui from './Settings/ui';
@ -14,6 +15,7 @@ import ui from './Settings/ui';
export * from './Settings/downloadClients';
export * from './Settings/general';
export * from './Settings/indexerCategories';
export * from './Settings/indexerProxies';
export * from './Settings/languages';
export * from './Settings/notifications';
export * from './Settings/applications';
@ -35,6 +37,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState,
general: general.defaultState,
indexerCategories: indexerCategories.defaultState,
indexerProxies: indexerProxies.defaultState,
languages: languages.defaultState,
notifications: notifications.defaultState,
applications: applications.defaultState,
@ -64,6 +67,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers,
...general.actionHandlers,
...indexerCategories.actionHandlers,
...indexerProxies.actionHandlers,
...languages.actionHandlers,
...notifications.actionHandlers,
...applications.actionHandlers,
@ -84,6 +88,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers,
...general.reducers,
...indexerCategories.reducers,
...indexerProxies.reducers,
...languages.reducers,
...notifications.reducers,
...applications.reducers,

@ -59,7 +59,7 @@ namespace NzbDrone.Common.Http.Dispatchers
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
}
webRequest.Proxy = GetProxy(request.Url);
webRequest.Proxy = request.Proxy ?? GetProxy(request.Url);
if (request.Headers != null)
{

@ -61,7 +61,7 @@ namespace NzbDrone.Common.Http
_cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient));
}
public async Task<HttpResponse> ExecuteAsync(HttpRequest request)
public virtual async Task<HttpResponse> ExecuteAsync(HttpRequest request)
{
var cookieContainer = InitializeRequestCookies(request);

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@ -31,6 +32,7 @@ namespace NzbDrone.Common.Http
public HttpMethod Method { get; set; }
public HttpHeader Headers { get; set; }
public Encoding Encoding { get; set; }
public IWebProxy Proxy { get; set; }
public byte[] ContentData { get; set; }
public string ContentSummary { get; set; }
public bool SuppressHttpError { get; set; }
@ -87,6 +89,19 @@ namespace NzbDrone.Common.Http
}
}
public string GetContent()
{
if (Encoding != null)
{
return Encoding.GetString(ContentData);
}
else
{
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
return encoding.GetString(ContentData);
}
}
public void AddBasicAuthentication(string username, string password)
{
var authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}"));

@ -33,9 +33,9 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
{
var recentFeed = ReadAllText(@"Files/Indexers/PrivateHD/recentfeed.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;

@ -32,9 +32,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
{
var recentFeed = ReadAllText(@"Files/Indexers/FileList/recentfeed.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;

@ -44,9 +44,9 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
{
var responseJson = ReadAllText(fileName);
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson)));
var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases;
@ -73,9 +73,9 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
{
var responseJson = new { status = 5, message = "Invalid authentication credentials" }.ToJson();
Mocker.GetMock<IHttpClient>()
.Setup(v => v.ExecuteAsync(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson))));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.IsAny<HttpRequest>(), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson))));
var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases;

@ -1,10 +1,12 @@
using System;
using System.Net;
using System.Threading.Tasks;
using System.Xml;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Test.Framework;
@ -14,6 +16,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
{
private NewznabSettings _settings;
private IndexerDefinition _definition;
private string _caps;
[SetUp]
@ -24,14 +27,24 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
BaseUrl = "http://indxer.local"
};
_definition = new IndexerDefinition()
{
Id = 5,
Name = "Newznab",
Settings = new NewznabSettings()
{
BaseUrl = "http://indexer.local/"
}
};
_caps = ReadAllText("Files/Indexers/Newznab/newznab_caps.xml");
}
private void GivenCapsResponse(string caps)
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), caps));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()))
.Returns<HttpRequest, IndexerDefinition>((r, d) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), caps));
}
[Test]
@ -39,11 +52,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps);
Subject.GetCapabilities(_settings);
Subject.GetCapabilities(_settings);
Subject.GetCapabilities(_settings, _definition);
Subject.GetCapabilities(_settings, _definition);
Mocker.GetMock<IHttpClient>()
.Verify(o => o.Get(It.IsAny<HttpRequest>()), Times.Once());
Mocker.GetMock<IIndexerHttpClient>()
.Verify(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()), Times.Once());
}
[Test]
@ -51,7 +64,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps);
var caps = Subject.GetCapabilities(_settings);
var caps = Subject.GetCapabilities(_settings, _definition);
caps.LimitsDefault.Value.Should().Be(25);
caps.LimitsMax.Value.Should().Be(60);
@ -62,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps.Replace("<limits", "<abclimits"));
var caps = Subject.GetCapabilities(_settings);
var caps = Subject.GetCapabilities(_settings, _definition);
caps.LimitsDefault.Value.Should().Be(100);
caps.LimitsMax.Value.Should().Be(100);
@ -71,11 +84,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test]
public void should_throw_if_failed_to_get()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Get(It.IsAny<HttpRequest>()))
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()))
.Throws<Exception>();
Assert.Throws<Exception>(() => Subject.GetCapabilities(_settings));
Assert.Throws<Exception>(() => Subject.GetCapabilities(_settings, _definition));
}
[Test]
@ -83,7 +96,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps.Replace("<limits", "<>"));
Assert.Throws<XmlException>(() => Subject.GetCapabilities(_settings));
Assert.Throws<XmlException>(() => Subject.GetCapabilities(_settings, _definition));
}
[Test]
@ -91,7 +104,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps.Replace("5030", "asdf"));
var result = Subject.GetCapabilities(_settings);
var result = Subject.GetCapabilities(_settings, _definition);
result.Should().NotBeNull();
}

@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps);
}
@ -42,9 +42,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Newznab/newznab_nzb_su.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 }, Limit = 100, Offset = 0 })).Releases;

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

@ -36,13 +36,13 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
Json.Serialize(authResponse, authStream);
var responseJson = ReadAllText(fileName);
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString())));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString())));
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson)));
var torrents = (await Subject.Fetch(new MovieSearchCriteria())).Releases;

@ -37,9 +37,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Rarbg/RecentFeed_v2.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
@ -64,9 +64,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
[Test]
public async Task should_parse_error_20_as_empty_results()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }")));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }")));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
@ -76,9 +76,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
[Test]
public async Task should_warn_on_unknown_error()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }")));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }")));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;

@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.IndexerTests
public int _supportedPageSize;
public override int PageSize => _supportedPageSize;
public TestIndexer(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
public TestIndexer(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
{
}

@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
_caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps);
}
@ -43,9 +43,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_hdaccess_net.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
@ -72,9 +72,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
@ -102,9 +102,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_animetosho.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases;

@ -0,0 +1,21 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(10)]
public class IndexerProxies : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("IndexerProxies")
.WithColumn("Name").AsString()
.WithColumn("Settings").AsString()
.WithColumn("Implementation").AsString()
.WithColumn("ConfigContract").AsString().Nullable()
.WithColumn("Tags").AsString().Nullable();
Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable();
}
}
}

@ -9,6 +9,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.CustomFilters;
using NzbDrone.Core.Datastore.Converters;
using NzbDrone.Core.Download;
using NzbDrone.Core.IndexerProxies;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Jobs;
@ -53,7 +54,6 @@ namespace NzbDrone.Core.Datastore
.Ignore(i => i.SupportsSearch)
.Ignore(i => i.SupportsRedirect)
.Ignore(i => i.Capabilities)
.Ignore(d => d.Tags)
.HasOne(a => a.AppProfile, a => a.AppProfileId);
Mapper.Entity<DownloadClientDefinition>("DownloadClients").RegisterModel()
@ -65,6 +65,9 @@ namespace NzbDrone.Core.Datastore
.Ignore(x => x.ImplementationName)
.Ignore(i => i.SupportsOnHealthIssue);
Mapper.Entity<IndexerProxyDefinition>("IndexerProxies").RegisterModel()
.Ignore(x => x.ImplementationName);
Mapper.Entity<ApplicationDefinition>("Applications").RegisterModel()
.Ignore(x => x.ImplementationName);

@ -0,0 +1,92 @@
using System;
using System.Linq;
using System.Net;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerProxies;
using NzbDrone.Core.Localization;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class IndexerProxyCheck : HealthCheckBase
{
private readonly Logger _logger;
private readonly IIndexerProxyFactory _proxyFactory;
private readonly IHttpClient _client;
private readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
public IndexerProxyCheck(IProwlarrCloudRequestBuilder cloudRequestBuilder,
IHttpClient client,
IIndexerProxyFactory proxyFactory,
ILocalizationService localizationService,
Logger logger)
: base(localizationService)
{
_proxyFactory = proxyFactory;
_cloudRequestBuilder = cloudRequestBuilder.Services;
_logger = logger;
_client = client;
}
public override HealthCheck Check()
{
var enabledProviders = _proxyFactory.GetAvailableProviders();
var badProxies = enabledProviders.Where(p => !IsProxyWorking(p)).ToList();
if (enabledProviders.Empty())
{
return new HealthCheck(GetType());
}
if (badProxies.Count == enabledProviders.Count)
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("IndexerProxyStatusCheckAllClientMessage"),
"#proxies-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerProxyStatusCheckSingleClientMessage"),
string.Join(", ", badProxies.Select(v => v.Definition.Name))),
"#proxies-are-unavailable-due-to-failures");
}
private bool IsProxyWorking(IIndexerProxy indexerProxy)
{
var request = _cloudRequestBuilder.Create()
.Resource("/ping")
.Build();
try
{
var addresses = Dns.GetHostAddresses(((IIndexerProxySettings)indexerProxy.Definition.Settings).Host);
if (!addresses.Any())
{
return false;
}
var response = _client.Execute(request);
// We only care about 400 responses, other error codes can be ignored
if (response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
return false;
}
}
catch (Exception ex)
{
_logger.Error(ex, "Proxy Health Check failed");
return false;
}
return true;
}
}
}

@ -0,0 +1,34 @@
using System.Net;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.IndexerProxies.Http
{
public class Http : HttpIndexerProxyBase<HttpSettings>
{
public Http(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger)
: base(cloudRequestBuilder, httpClient, logger)
{
}
public override string Name => "Http";
public override HttpRequest PreRequest(HttpRequest request)
{
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace())
{
request.Proxy = new WebProxy(Settings.Host + ":" + Settings.Port, false, null, new NetworkCredential(Settings.Username, Settings.Password));
}
else
{
request.Proxy = new WebProxy(Settings.Host + ":" + Settings.Port, false, null);
}
_logger.Debug("Applying HTTP(S) Proxy {0} to request {1}", Name, request.Url);
return request;
}
}
}

@ -0,0 +1,43 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.IndexerProxies.Http
{
public class HttpSettingsValidator : AbstractValidator<HttpSettings>
{
public HttpSettingsValidator()
{
RuleFor(c => c.Host).NotEmpty();
RuleFor(c => c.Port).NotEmpty();
}
}
public class HttpSettings : IIndexerProxySettings
{
private static readonly HttpSettingsValidator Validator = new HttpSettingsValidator();
public HttpSettings()
{
Username = "";
Password = "";
}
[FieldDefinition(0, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Http;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.IndexerProxies
{
public abstract class HttpIndexerProxyBase<TSettings> : IndexerProxyBase<TSettings>
where TSettings : IIndexerProxySettings, new()
{
protected readonly IHttpClient _httpClient;
protected readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
protected readonly Logger _logger;
public HttpIndexerProxyBase(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
_cloudRequestBuilder = cloudRequestBuilder.Services;
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
var addresses = Dns.GetHostAddresses(Settings.Host);
if (!addresses.Any())
{
failures.Add(new NzbDroneValidationFailure("Host", "ProxyCheckResolveIpMessage"));
}
var request = PreRequest(_cloudRequestBuilder.Create()
.Resource("/ping")
.Build());
try
{
var response = _httpClient.Execute(request);
// We only care about 400 responses, other error codes can be ignored
if (response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
failures.Add(new NzbDroneValidationFailure("Host", "ProxyCheckBadRequestMessage"));
}
}
catch (Exception ex)
{
_logger.Error(ex, "Proxy Health Check failed");
failures.Add(new NzbDroneValidationFailure("Host", "ProxyCheckFailedToTestMessage"));
}
return new ValidationResult(failures);
}
}
}

@ -0,0 +1,11 @@
using NzbDrone.Common.Http;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.IndexerProxies
{
public interface IIndexerProxy : IProvider
{
HttpRequest PreRequest(HttpRequest request);
HttpResponse PostResponse(HttpResponse response);
}
}

@ -0,0 +1,9 @@
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.IndexerProxies
{
public interface IIndexerProxySettings : IProviderConfig
{
string Host { get; set; }
}
}

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Http;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.IndexerProxies
{
public abstract class IndexerProxyBase<TSettings> : IIndexerProxy
where TSettings : IIndexerProxySettings, new()
{
public abstract string Name { get; }
public virtual ProviderMessage Message => null;
public Type ConfigContract => typeof(TSettings);
public IEnumerable<ProviderDefinition> DefaultDefinitions => new List<ProviderDefinition>();
public ProviderDefinition Definition { get; set; }
public abstract ValidationResult Test();
public abstract HttpRequest PreRequest(HttpRequest request);
public virtual HttpResponse PostResponse(HttpResponse response)
{
return response;
}
protected TSettings Settings => (TSettings)Definition.Settings;
public override string ToString()
{
return GetType().Name;
}
public virtual object RequestAction(string action, IDictionary<string, string> query)
{
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;
}
}
}

@ -0,0 +1,10 @@
using System.Linq;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.IndexerProxies
{
public class IndexerProxyDefinition : ProviderDefinition
{
public override bool Enable => Tags.Any();
}
}

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.IndexerProxies
{
public interface IIndexerProxyFactory : IProviderFactory<IIndexerProxy, IndexerProxyDefinition>
{
}
public class IndexerProxyFactory : ProviderFactory<IIndexerProxy, IndexerProxyDefinition>, IIndexerProxyFactory
{
public IndexerProxyFactory(IIndexerProxyRepository providerRepository, IEnumerable<IIndexerProxy> providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)
{
}
}
}

@ -0,0 +1,24 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.IndexerProxies
{
public interface IIndexerProxyRepository : IProviderRepository<IndexerProxyDefinition>
{
void UpdateSettings(IndexerProxyDefinition model);
}
public class IndexerProxyRepository : ProviderRepository<IndexerProxyDefinition>, IIndexerProxyRepository
{
public IndexerProxyRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public void UpdateSettings(IndexerProxyDefinition model)
{
SetFields(model, m => m.Settings);
}
}
}

@ -0,0 +1,19 @@
using System;
using NLog;
using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.IndexerProxies
{
public class IndexerProxyService
{
private readonly IIndexerProxyFactory _indexerProxyFactory;
private readonly Logger _logger;
public IndexerProxyService(IIndexerProxyFactory indexerProxyFactory, Logger logger)
{
_indexerProxyFactory = indexerProxyFactory;
_logger = logger;
}
}
}

@ -0,0 +1,66 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using com.LandonKey.SocksWebProxy;
using com.LandonKey.SocksWebProxy.Proxy;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.IndexerProxies.Socks4
{
public class Socks4 : HttpIndexerProxyBase<Socks4Settings>
{
public Socks4(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger)
: base(cloudRequestBuilder, httpClient, logger)
{
}
public override string Name => "Socks4";
public override HttpRequest PreRequest(HttpRequest request)
{
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace())
{
request.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(Settings.Host), Settings.Port, ProxyConfig.SocksVersion.Four), false);
}
else
{
request.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(Settings.Host), Settings.Port, ProxyConfig.SocksVersion.Four, Settings.Username, Settings.Password), false);
}
_logger.Debug("Applying Socks4 Proxy {0} to request {1}", Name, request.Url);
return request;
}
private static int GetNextFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static IPAddress GetProxyIpAddress(string host)
{
IPAddress ipAddress;
if (!IPAddress.TryParse(host, out ipAddress))
{
try
{
ipAddress = Dns.GetHostEntry(host).AddressList.OrderByDescending(a => a.AddressFamily == AddressFamily.InterNetwork).First();
}
catch (Exception e)
{
throw new InvalidOperationException(string.Format("Unable to resolve proxy hostname '{0}' to a valid IP address.", host), e);
}
}
return ipAddress;
}
}
}

@ -0,0 +1,43 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.IndexerProxies.Socks4
{
public class Socks4SettingsValidator : AbstractValidator<Socks4Settings>
{
public Socks4SettingsValidator()
{
RuleFor(c => c.Host).NotEmpty();
RuleFor(c => c.Port).NotEmpty();
}
}
public class Socks4Settings : IIndexerProxySettings
{
private static readonly Socks4SettingsValidator Validator = new Socks4SettingsValidator();
public Socks4Settings()
{
Username = "";
Password = "";
}
[FieldDefinition(0, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -0,0 +1,67 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using com.LandonKey.SocksWebProxy;
using com.LandonKey.SocksWebProxy.Proxy;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.IndexerProxies.Socks5
{
public class Socks5 : HttpIndexerProxyBase<Socks5Settings>
{
public Socks5(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger)
: base(cloudRequestBuilder, httpClient, logger)
{
}
public override string Name => "Socks5";
public override HttpRequest PreRequest(HttpRequest request)
{
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace())
{
request.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(Settings.Host), Settings.Port, ProxyConfig.SocksVersion.Five), false);
}
else
{
request.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(Settings.Host), Settings.Port, ProxyConfig.SocksVersion.Five, Settings.Username, Settings.Password), false);
}
_logger.Debug("Applying Socks5 Proxy {0} to request {1}", Name, request.Url);
return request;
}
private static int GetNextFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static IPAddress GetProxyIpAddress(string host)
{
IPAddress ipAddress;
if (!IPAddress.TryParse(host, out ipAddress))
{
try
{
ipAddress = Dns.GetHostEntry(host).AddressList.OrderByDescending(a => a.AddressFamily == AddressFamily.InterNetwork).First();
}
catch (Exception e)
{
throw new InvalidOperationException(string.Format("Unable to resolve proxy hostname '{0}' to a valid IP address.", host), e);
}
}
return ipAddress;
}
}
}

@ -0,0 +1,43 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.IndexerProxies.Socks5
{
public class Socks5SettingsValidator : AbstractValidator<Socks5Settings>
{
public Socks5SettingsValidator()
{
RuleFor(c => c.Host).NotEmpty();
RuleFor(c => c.Port).NotEmpty();
}
}
public class Socks5Settings : IIndexerProxySettings
{
private static readonly Socks5SettingsValidator Validator = new Socks5SettingsValidator();
public Socks5Settings()
{
Username = "";
Password = "";
}
[FieldDefinition(0, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -15,7 +15,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Language => "en-us";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public Aither(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public Aither(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -13,7 +13,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "AlphaRatio(AR) is a Private Torrent Tracker for 0DAY / GENERAL";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public AlphaRatio(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public AlphaRatio(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -33,7 +33,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.SemiPublic;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Anidub(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public Anidub(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
@ -58,7 +58,7 @@ namespace NzbDrone.Core.Indexers.Definitions
AllowAutoRedirect = true
};
var mainPage = await _httpClient.ExecuteAsync(new HttpRequest(Settings.BaseUrl));
var mainPage = await ExecuteAuth(new HttpRequest(Settings.BaseUrl));
requestBuilder.Method = Common.Http.HttpMethod.POST;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
@ -254,7 +254,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
private readonly AnidubSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public IHttpClient HttpClient { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public Logger Logger { get; set; }
private static Dictionary<string, string> CategoriesMap => new Dictionary<string, string>

@ -28,7 +28,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Anilibria(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public Anilibria(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -34,7 +34,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public AnimeBytes(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public AnimeBytes(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -31,7 +31,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public AnimeTorrents(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public AnimeTorrents(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
@ -56,7 +56,7 @@ namespace NzbDrone.Core.Indexers.Definitions
AllowAutoRedirect = true
};
var loginPage = await _httpClient.ExecuteAsync(new HttpRequest(LoginUrl));
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
requestBuilder.Method = HttpMethod.POST;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
requestBuilder.SetCookies(loginPage.GetCookies());

@ -15,7 +15,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Language => "de-de";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public AnimeWorld(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public AnimeWorld(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -29,7 +29,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Animedia(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public Animedia(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
@ -153,7 +153,7 @@ namespace NzbDrone.Core.Indexers.Definitions
private static readonly Regex CategorieMovieRegex = new Regex(@"Фильм", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex CategorieOVARegex = new Regex(@"ОВА|OVA|ОНА|ONA|Special", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex CategorieDoramaRegex = new Regex(@"Дорама", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public IHttpClient HttpClient { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public Logger Logger { get; set; }
public AnimediaParser(AnimediaSettings settings, IndexerCapabilitiesCategories categories)

@ -33,7 +33,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Anthelion(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public Anthelion(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "Aka AsiaTorrents";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public AvistaZ(IIndexerRepository indexerRepository, IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public AvistaZ(IIndexerRepository indexerRepository, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(indexerRepository, httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -21,7 +21,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
private IIndexerRepository _indexerRepository;
public AvistazBase(IIndexerRepository indexerRepository,
IHttpClient httpClient,
IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,

@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public AvistazSettings Settings { get; set; }
public IDictionary<string, string> AuthCookieCache { get; set; }
public IHttpClient HttpClient { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public Logger Logger { get; set; }

@ -32,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public BB(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public BB(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -31,7 +31,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public BakaBT(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public BakaBT(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
@ -56,7 +56,7 @@ namespace NzbDrone.Core.Indexers.Definitions
AllowAutoRedirect = true
};
var loginPage = await _httpClient.ExecuteAsync(new HttpRequest(LoginUrl));
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
requestBuilder.Method = HttpMethod.POST;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
@ -78,7 +78,7 @@ namespace NzbDrone.Core.Indexers.Definitions
.SetHeader("Content-Type", "multipart/form-data")
.Build();
var response = await _httpClient.ExecuteAsync(authLoginRequest);
var response = await ExecuteAuth(authLoginRequest);
if (response.Content != null && response.Content.Contains("<a href=\"logout.php\">Logout</a>"))
{

@ -30,7 +30,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public BeyondHD(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public BeyondHD(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -31,7 +31,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override bool SupportsRss => false;
public override IndexerCapabilities Capabilities => SetCapabilities();
public BinSearch(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
public BinSearch(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
{
}

@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "Blutopia (BLU) is a Private Torrent Tracker for HD MOVIES / TV";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public Blutopia(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public Blutopia(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -20,7 +20,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public override string[] IndexerUrls => new string[] { "http://api.broadcasthe.net/" };
public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows";
public BroadcastheNet(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public BroadcastheNet(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -12,7 +12,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "Broken Stones is a Private site for MacOS and iOS APPS / GAMES";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public BrokenStones(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public BrokenStones(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -12,7 +12,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "CGPeers is a Private Torrent Tracker for GRAPHICS SOFTWARE / TUTORIALS / ETC";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public CGPeers(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public CGPeers(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -41,6 +41,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
_logger)
{
HttpClient = _httpClient,
Definition = Definition,
Settings = Settings
});
@ -85,7 +86,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
public Cardigann(IIndexerDefinitionUpdateService definitionService,
IHttpClient httpClient,
IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
@ -176,7 +177,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
try
{
var response = await _httpClient.ExecuteAsync(request);
var response = await _httpClient.ExecuteAsync(request, Definition);
downloadBytes = response.ResponseData;
}
catch (HttpException ex)

@ -13,12 +13,14 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Definitions.Cardigann;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers.Cardigann
{
public class CardigannRequestGenerator : CardigannBase, IIndexerRequestGenerator
{
public IHttpClient HttpClient { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public ProviderDefinition Definition { get; set; }
public IDictionary<string, string> Cookies { get; set; }
protected HttpResponse landingResult;
protected IHtmlDocument landingResultDocument;
@ -192,7 +194,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
requestBuilder.Headers.Add("Referer", SiteLink);
var response = await HttpClient.ExecuteAsync(requestBuilder.Build());
var response = await HttpClient.ExecuteAsync(requestBuilder.Build(), Definition);
Cookies = response.GetCookies();
@ -329,7 +331,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
requestBuilder.Headers.Add("Referer", loginUrl);
var simpleCaptchaResult = await HttpClient.ExecuteAsync(requestBuilder.Build());
var simpleCaptchaResult = await HttpClient.ExecuteAsync(requestBuilder.Build(), Definition);
var simpleCaptchaJSON = JObject.Parse(simpleCaptchaResult.Content);
var captchaSelection = simpleCaptchaJSON["images"][0]["hash"].ToString();
@ -409,7 +411,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
var request = requestBuilder.Build();
request.SetContent(body);
loginResult = await HttpClient.ExecuteAsync(request);
loginResult = await HttpClient.ExecuteAsync(request, Definition);
}
else
{
@ -429,7 +431,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
requestBuilder.AddFormParameter(pair.Key, pair.Value);
}
loginResult = await HttpClient.ExecuteAsync(requestBuilder.Build());
loginResult = await HttpClient.ExecuteAsync(requestBuilder.Build(), Definition);
}
Cookies = loginResult.GetCookies();
@ -464,7 +466,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
requestBuilder.Headers.Add("Referer", SiteLink);
var response = await HttpClient.ExecuteAsync(requestBuilder.Build());
var response = await HttpClient.ExecuteAsync(requestBuilder.Build(), Definition);
Cookies = response.GetCookies();
@ -488,7 +490,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
requestBuilder.Headers.Add("Referer", SiteLink);
var response = await HttpClient.ExecuteAsync(requestBuilder.Build());
var response = await HttpClient.ExecuteAsync(requestBuilder.Build(), Definition);
Cookies = response.GetCookies();
@ -567,7 +569,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
var request = requestBuilder.Build();
landingResult = await HttpClient.ExecuteAsync(request);
landingResult = await HttpClient.ExecuteAsync(request, Definition);
Cookies = landingResult.GetCookies();
@ -611,7 +613,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
.SetHeader("Referer", loginUrl.AbsoluteUri)
.Build();
var response = await HttpClient.ExecuteAsync(request);
var response = await HttpClient.ExecuteAsync(request, Definition);
return new Captcha
{
@ -701,7 +703,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
}
var response = await HttpClient.ExecuteAsync(httpRequest.Build());
var response = await HttpClient.ExecuteAsync(httpRequest.Build(), Definition);
_logger.Debug($"CardigannIndexer ({_definition.Id}): handleRequest() remote server returned {response.StatusCode.ToString()}");
return response;
@ -741,7 +743,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
request.AllowAutoRedirect = true;
var response = await HttpClient.ExecuteAsync(request);
var response = await HttpClient.ExecuteAsync(request, Definition);
var results = response.Content;
var searchResultParser = new HtmlParser();

@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "CinemaZ (EuTorrents) is a Private Torrent Tracker for FOREIGN NON-ASIAN MOVIES.";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public CinemaZ(IIndexerRepository indexerRepository, IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public CinemaZ(IIndexerRepository indexerRepository, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(indexerRepository, httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -28,7 +28,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public DanishBytes(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public DanishBytes(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -29,7 +29,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public DigitalCore(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public DigitalCore(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Description => "ExoticaZ (YourExotic) is a Private Torrent Tracker for 3X";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public ExoticaZ(IIndexerRepository indexerRepository, IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public ExoticaZ(IIndexerRepository indexerRepository, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(indexerRepository, httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -18,7 +18,7 @@ namespace NzbDrone.Core.Indexers.FileList
public override bool SupportsRedirect => true;
public override IndexerCapabilities Capabilities => SetCapabilities();
public FileList(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public FileList(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -17,7 +17,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
public override int PageSize => 50;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Gazelle(IHttpClient httpClient,
public Gazelle(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,

@ -11,7 +11,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
public GazelleSettings Settings { get; set; }
public IDictionary<string, string> AuthCookieCache { get; set; }
public IHttpClient HttpClient { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public Logger Logger { get; set; }

@ -30,7 +30,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public GazelleGames(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public GazelleGames(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -18,7 +18,7 @@ namespace NzbDrone.Core.Indexers.HDBits
public override int PageSize => 30;
public HDBits(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public HDBits(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -32,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public HDSpace(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public HDSpace(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
@ -49,7 +49,7 @@ namespace NzbDrone.Core.Indexers.Definitions
protected override async Task DoLogin()
{
var loginPage = await _httpClient.ExecuteAsync(new HttpRequest(LoginUrl));
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{

@ -30,7 +30,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public HDTorrents(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public HDTorrents(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -36,7 +36,7 @@ namespace NzbDrone.Core.Indexers.Headphones
return new HeadphonesRssParser(Capabilities.Categories);
}
public Headphones(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
public Headphones(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
{
}
@ -63,7 +63,7 @@ namespace NzbDrone.Core.Indexers.Headphones
try
{
var response = await _httpClient.ExecuteAsync(request);
var response = await _httpClient.ExecuteAsync(request, Definition);
downloadBytes = response.ResponseData;
}
catch (Exception)

@ -40,7 +40,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public IPTorrents(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public IPTorrents(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -32,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public ImmortalSeed(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public ImmortalSeed(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -33,7 +33,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override bool FollowRedirect => true;
public InternetArchive(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public InternetArchive(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -25,7 +25,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Milkie(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public Milkie(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -27,7 +27,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public MyAnonamouse(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public MyAnonamouse(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

@ -31,7 +31,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Nebulance(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public Nebulance(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}

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

Loading…
Cancel
Save