mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
23 Commits
v0.4.10.21
...
v0.4.11.21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57dcd861a9 | ||
|
|
dfe132cda2 | ||
|
|
a635820b48 | ||
|
|
d959e81efb | ||
|
|
ac89cd636f | ||
|
|
50616f5c9e | ||
|
|
3f9cb2c6ea | ||
|
|
b5aa85a548 | ||
|
|
0fa5127c83 | ||
|
|
4d137886bc | ||
|
|
9dde041c99 | ||
|
|
a8234c9ce0 | ||
|
|
9227efdb65 | ||
|
|
fa923e658f | ||
|
|
364a5564ae | ||
|
|
9efd0b391e | ||
|
|
320161e051 | ||
|
|
38ba810ae8 | ||
|
|
4e3f460a24 | ||
|
|
0d918a0aa9 | ||
|
|
a110412665 | ||
|
|
6c97f1b6ee | ||
|
|
470779ead2 |
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.4.10'
|
||||
majorVersion: '0.4.11'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
|
||||
@@ -16,6 +16,7 @@ import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||
import InfoInput from './InfoInput';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import NewznabCategorySelectInputConnector from './NewznabCategorySelectInputConnector';
|
||||
import NumberInput from './NumberInput';
|
||||
import OAuthInputConnector from './OAuthInputConnector';
|
||||
import PasswordInput from './PasswordInput';
|
||||
@@ -68,6 +69,9 @@ function getComponent(type) {
|
||||
case inputTypes.PATH:
|
||||
return PathInputConnector;
|
||||
|
||||
case inputTypes.CATEGORY_SELECT:
|
||||
return NewznabCategorySelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||
return IndexerFlagsSelectInputConnector;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ function createMapStateToProps() {
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
value: value || [],
|
||||
values
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export const DEVICE = 'device';
|
||||
export const KEY_VALUE_LIST = 'keyValueList';
|
||||
export const INFO = 'info';
|
||||
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
||||
export const CATEGORY_SELECT = 'newznabCategorySelect';
|
||||
export const NUMBER = 'number';
|
||||
export const OAUTH = 'oauth';
|
||||
export const PASSWORD = 'password';
|
||||
@@ -32,6 +33,7 @@ export const all = [
|
||||
KEY_VALUE_LIST,
|
||||
INFO,
|
||||
MOVIE_MONITORED_SELECT,
|
||||
CATEGORY_SELECT,
|
||||
NUMBER,
|
||||
OAUTH,
|
||||
PASSWORD,
|
||||
|
||||
@@ -123,7 +123,7 @@ class AddIndexerModalContent extends Component {
|
||||
const filteredIndexers = indexers.filter((indexer) => {
|
||||
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
||||
|
||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@ class IndexerIndex extends Component {
|
||||
saveError,
|
||||
isDeleting,
|
||||
isTestingAll,
|
||||
isSyncingIndexers,
|
||||
deleteError,
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
@@ -309,6 +310,15 @@ class IndexerIndex extends Component {
|
||||
onPress={this.onAddIndexerPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('SyncAppIndexers')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isSyncingIndexers}
|
||||
onPress={this.props.onAppIndexerSyncPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('TestAllIndexers')}
|
||||
iconName={icons.TEST}
|
||||
@@ -493,10 +503,12 @@ IndexerIndex.propTypes = {
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
isTestingAll: PropTypes.bool.isRequired,
|
||||
isSyncingIndexers: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onTestAllPress: PropTypes.func.isRequired,
|
||||
onAppIndexerSyncPress: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -2,10 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { testAllIndexers } from 'Store/Actions/indexerActions';
|
||||
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
|
||||
import IndexerIndex from './IndexerIndex';
|
||||
@@ -13,13 +16,16 @@ import IndexerIndex from './IndexerIndex';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
|
||||
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
indexers,
|
||||
isSyncingIndexers,
|
||||
dimensionsState
|
||||
) => {
|
||||
return {
|
||||
...indexers,
|
||||
isSyncingIndexers,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
}
|
||||
@@ -46,6 +52,12 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
|
||||
onTestAllPress() {
|
||||
dispatch(testAllIndexers());
|
||||
},
|
||||
|
||||
onAppIndexerSyncPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.APP_INDEXER_SYNC
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AddCategoryModalContentConnector from './AddCategoryModalContentConnector';
|
||||
|
||||
function AddCategoryModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddCategoryModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddCategoryModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddCategoryModal;
|
||||
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import AddCategoryModal from './AddCategoryModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const section = 'settings.downloadClientCategories';
|
||||
|
||||
return {
|
||||
dispatchClearPendingChanges() {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class AddCategoryModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPendingChanges();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AddCategoryModal
|
||||
{...otherProps}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddCategoryModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(AddCategoryModalConnector);
|
||||
@@ -0,0 +1,5 @@
|
||||
.deleteButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AddCategoryModalContent.css';
|
||||
|
||||
function AddCategoryModalContent(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
item,
|
||||
onInputChange,
|
||||
onFieldChange,
|
||||
onCancelPress,
|
||||
onSavePress,
|
||||
onDeleteSpecificationPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
id,
|
||||
clientCategory,
|
||||
categories
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onCancelPress}>
|
||||
<ModalHeader>
|
||||
{`${id ? 'Edit' : 'Add'} Category`}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('DownloadClientCategory')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="clientCategory"
|
||||
{...clientCategory}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MappedCategories')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CATEGORY_SELECT}
|
||||
name="categories"
|
||||
{...categories}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteSpecificationPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onCancelPress}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={false}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AddCategoryModalContent.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onCancelPress: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onDeleteSpecificationPress: PropTypes.func
|
||||
};
|
||||
|
||||
export default AddCategoryModalContent;
|
||||
@@ -0,0 +1,78 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearDownloadClientCategoryPending, saveDownloadClientCategory, setDownloadClientCategoryFieldValue, setDownloadClientCategoryValue } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import AddCategoryModalContent from './AddCategoryModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('downloadClientCategories'),
|
||||
(advancedSettings, specification) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...specification
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setDownloadClientCategoryValue,
|
||||
setDownloadClientCategoryFieldValue,
|
||||
saveDownloadClientCategory,
|
||||
clearDownloadClientCategoryPending
|
||||
};
|
||||
|
||||
class AddCategoryModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setDownloadClientCategoryValue({ name, value });
|
||||
};
|
||||
|
||||
onFieldChange = ({ name, value }) => {
|
||||
this.props.setDownloadClientCategoryFieldValue({ name, value });
|
||||
};
|
||||
|
||||
onCancelPress = () => {
|
||||
this.props.clearDownloadClientCategoryPending();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveDownloadClientCategory({ id: this.props.id });
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddCategoryModalContent
|
||||
{...this.props}
|
||||
onCancelPress={this.onCancelPress}
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddCategoryModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
item: PropTypes.object.isRequired,
|
||||
setDownloadClientCategoryValue: PropTypes.func.isRequired,
|
||||
setDownloadClientCategoryFieldValue: PropTypes.func.isRequired,
|
||||
clearDownloadClientCategoryPending: PropTypes.func.isRequired,
|
||||
saveDownloadClientCategory: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddCategoryModalContentConnector);
|
||||
@@ -0,0 +1,32 @@
|
||||
.customFormat {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 5px;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.labels {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddCategoryModalConnector from './AddCategoryModalConnector';
|
||||
import styles from './Category.css';
|
||||
|
||||
class Category extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditSpecificationModalOpen: false,
|
||||
isDeleteSpecificationModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditSpecificationPress = () => {
|
||||
this.setState({ isEditSpecificationModalOpen: true });
|
||||
};
|
||||
|
||||
onEditSpecificationModalClose = () => {
|
||||
this.setState({ isEditSpecificationModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteSpecificationPress = () => {
|
||||
this.setState({
|
||||
isEditSpecificationModalOpen: false,
|
||||
isDeleteSpecificationModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteSpecificationModalClose = () => {
|
||||
this.setState({ isDeleteSpecificationModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmDeleteSpecification = () => {
|
||||
this.props.onConfirmDeleteSpecification(this.props.id);
|
||||
};
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
clientCategory,
|
||||
categories
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.customFormat}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditSpecificationPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{clientCategory}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Label kind={kinds.PRIMARY}>
|
||||
{`${categories.length} ${categories.length > 1 ? translate('Categories') : translate('Category')}`}
|
||||
</Label>
|
||||
|
||||
<AddCategoryModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditSpecificationModalOpen}
|
||||
onModalClose={this.onEditSpecificationModalClose}
|
||||
onDeleteSpecificationPress={this.onDeleteSpecificationPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteSpecificationModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteClientCategory')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('AreYouSureYouWantToDeleteCategory', [name])}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteSpecification}
|
||||
onCancel={this.onDeleteSpecificationModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Category.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
categories: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
clientCategory: PropTypes.string.isRequired,
|
||||
onConfirmDeleteSpecification: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Category;
|
||||
@@ -1,11 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -13,12 +16,33 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddCategoryModalConnector from './Categories/AddCategoryModalConnector';
|
||||
import Category from './Categories/Category';
|
||||
import styles from './EditDownloadClientModalContent.css';
|
||||
|
||||
class EditDownloadClientModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddCategoryModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
onAddCategoryPress = () => {
|
||||
this.setState({ isAddCategoryModalOpen: true });
|
||||
};
|
||||
|
||||
onAddCategoryModalClose = () => {
|
||||
this.setState({ isAddCategoryModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -27,6 +51,7 @@ class EditDownloadClientModalContent extends Component {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
categories,
|
||||
isSaving,
|
||||
isTesting,
|
||||
saveError,
|
||||
@@ -37,15 +62,21 @@ class EditDownloadClientModalContent extends Component {
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onDeleteDownloadClientPress,
|
||||
onConfirmDeleteCategory,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isAddCategoryModalOpen
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
enable,
|
||||
priority,
|
||||
supportsCategories,
|
||||
fields,
|
||||
message
|
||||
} = item;
|
||||
@@ -136,6 +167,43 @@ class EditDownloadClientModalContent extends Component {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
supportsCategories.value ?
|
||||
<FieldSet legend={translate('MappedCategories')}>
|
||||
<div className={styles.customFormats}>
|
||||
{
|
||||
categories.map((tag) => {
|
||||
return (
|
||||
<Category
|
||||
key={tag.id}
|
||||
{...tag}
|
||||
onConfirmDeleteSpecification={onConfirmDeleteCategory}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addCategory}
|
||||
onPress={this.onAddCategoryPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={25}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
|
||||
<AddCategoryModalConnector
|
||||
isOpen={isAddCategoryModalOpen}
|
||||
onModalClose={this.onAddCategoryModalClose}
|
||||
/>
|
||||
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
@@ -185,13 +253,15 @@ EditDownloadClientModalContent.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isTesting: PropTypes.bool.isRequired,
|
||||
categories: PropTypes.arrayOf(PropTypes.object),
|
||||
item: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onDeleteDownloadClientPress: PropTypes.func
|
||||
onDeleteDownloadClientPress: PropTypes.func,
|
||||
onConfirmDeleteCategory: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditDownloadClientModalContent;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||
import { deleteDownloadClientCategory, fetchDownloadClientCategories, saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||
|
||||
@@ -10,10 +10,12 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('downloadClients'),
|
||||
(advancedSettings, downloadClient) => {
|
||||
(state) => state.settings.downloadClientCategories,
|
||||
(advancedSettings, downloadClient, categories) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...downloadClient
|
||||
...downloadClient,
|
||||
categories: categories.items
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -23,7 +25,9 @@ const mapDispatchToProps = {
|
||||
setDownloadClientValue,
|
||||
setDownloadClientFieldValue,
|
||||
saveDownloadClient,
|
||||
testDownloadClient
|
||||
testDownloadClient,
|
||||
fetchDownloadClientCategories,
|
||||
deleteDownloadClientCategory
|
||||
};
|
||||
|
||||
class EditDownloadClientModalContentConnector extends Component {
|
||||
@@ -31,6 +35,14 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
id,
|
||||
tagsFromId
|
||||
} = this.props;
|
||||
this.props.fetchDownloadClientCategories({ id: tagsFromId || id });
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
@@ -56,6 +68,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
this.props.testDownloadClient({ id: this.props.id });
|
||||
};
|
||||
|
||||
onConfirmDeleteCategory = (id) => {
|
||||
this.props.deleteDownloadClientCategory({ id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -67,6 +83,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
onTestPress={this.onTestPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
onConfirmDeleteCategory={this.onConfirmDeleteCategory}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -74,10 +91,13 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
|
||||
EditDownloadClientModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
tagsFromId: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
fetchDownloadClientCategories: PropTypes.func.isRequired,
|
||||
deleteDownloadClientCategory: PropTypes.func.isRequired,
|
||||
setDownloadClientValue: PropTypes.func.isRequired,
|
||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||
saveDownloadClient: PropTypes.func.isRequired,
|
||||
|
||||
@@ -14,8 +14,6 @@ function createLanguagesSelector() {
|
||||
return createSelector(
|
||||
(state) => state.localization,
|
||||
(localization) => {
|
||||
console.log(localization);
|
||||
|
||||
const items = localization.items;
|
||||
|
||||
if (!items) {
|
||||
|
||||
169
frontend/src/Store/Actions/Settings/downloadClientCategories.js
Normal file
169
frontend/src/Store/Actions/Settings/downloadClientCategories.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getNextId from 'Utilities/State/getNextId';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||
import { removeItem, set, update, updateItem } from '../baseActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.downloadClientCategories';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/fetchDownloadClientCategories';
|
||||
export const FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/fetchDownloadClientCategorySchema';
|
||||
export const SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/selectDownloadClientCategorySchema';
|
||||
export const SET_DOWNLOAD_CLIENT_CATEGORY_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryValue';
|
||||
export const SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryFieldValue';
|
||||
export const SAVE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/saveDownloadClientCategory';
|
||||
export const DELETE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteDownloadClientCategory';
|
||||
export const DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteAllDownloadClientCategory';
|
||||
export const CLEAR_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/clearDownloadClientCategories';
|
||||
export const CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING = 'settings/downloadClientCategories/clearDownloadClientCategoryPending';
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchDownloadClientCategories = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORIES);
|
||||
export const fetchDownloadClientCategorySchema = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
|
||||
export const selectDownloadClientCategorySchema = createAction(SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
|
||||
|
||||
export const saveDownloadClientCategory = createThunk(SAVE_DOWNLOAD_CLIENT_CATEGORY);
|
||||
export const deleteDownloadClientCategory = createThunk(DELETE_DOWNLOAD_CLIENT_CATEGORY);
|
||||
export const deleteAllDownloadClientCategory = createThunk(DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY);
|
||||
|
||||
export const setDownloadClientCategoryValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const setDownloadClientCategoryFieldValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const clearDownloadClientCategory = createAction(CLEAR_DOWNLOAD_CLIENT_CATEGORIES);
|
||||
|
||||
export const clearDownloadClientCategoryPending = createThunk(CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: [],
|
||||
selectedSchema: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_DOWNLOAD_CLIENT_CATEGORIES]: (getState, payload, dispatch) => {
|
||||
let tags = [];
|
||||
if (payload.id) {
|
||||
const cfState = getSectionState(getState(), 'settings.downloadClients', true);
|
||||
const cf = cfState.items[cfState.itemMap[payload.id]];
|
||||
tags = cf.categories.map((tag, i) => {
|
||||
return {
|
||||
id: i + 1,
|
||||
...tag
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
update({ section, data: tags }),
|
||||
set({
|
||||
section,
|
||||
isPopulated: true
|
||||
})
|
||||
]));
|
||||
},
|
||||
|
||||
[SAVE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
||||
const {
|
||||
id,
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
|
||||
|
||||
// we have to set id since not actually posting to server yet
|
||||
if (!saveData.id) {
|
||||
saveData.id = getNextId(getState().settings.downloadClientCategories.items);
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
updateItem({ section, ...saveData }),
|
||||
set({
|
||||
section,
|
||||
pendingChanges: {}
|
||||
})
|
||||
]));
|
||||
},
|
||||
|
||||
[DELETE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
||||
const id = payload.id;
|
||||
return dispatch(removeItem({ section, id }));
|
||||
},
|
||||
|
||||
[DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
|
||||
return dispatch(set({
|
||||
section,
|
||||
items: []
|
||||
}));
|
||||
},
|
||||
|
||||
[CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING]: (getState, payload, dispatch) => {
|
||||
return dispatch(set({
|
||||
section,
|
||||
pendingChanges: {}
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_DOWNLOAD_CLIENT_CATEGORY_VALUE]: createSetSettingValueReducer(section),
|
||||
[SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
||||
|
||||
[SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA]: (state, { payload }) => {
|
||||
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||
return selectedSchema;
|
||||
});
|
||||
},
|
||||
|
||||
[CLEAR_DOWNLOAD_CLIENT_CATEGORIES]: createClearReducer(section, {
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||
import { set } from '../baseActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
@@ -90,10 +91,34 @@ export default {
|
||||
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
|
||||
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
|
||||
|
||||
[SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler(section, '/downloadclient'),
|
||||
[SAVE_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
|
||||
// move the format tags in as a pending change
|
||||
const state = getState();
|
||||
const pendingChanges = state.settings.downloadClients.pendingChanges;
|
||||
pendingChanges.categories = state.settings.downloadClientCategories.items;
|
||||
dispatch(set({
|
||||
section,
|
||||
pendingChanges
|
||||
}));
|
||||
|
||||
createSaveProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
|
||||
},
|
||||
|
||||
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
|
||||
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
|
||||
[TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'),
|
||||
|
||||
[TEST_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
|
||||
const state = getState();
|
||||
const pendingChanges = state.settings.downloadClients.pendingChanges;
|
||||
pendingChanges.categories = state.settings.downloadClientCategories.items;
|
||||
dispatch(set({
|
||||
section,
|
||||
pendingChanges
|
||||
}));
|
||||
|
||||
createTestProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
|
||||
},
|
||||
|
||||
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
|
||||
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
|
||||
},
|
||||
|
||||
@@ -310,8 +310,6 @@ export const actionHandlers = handleThunks({
|
||||
isGrabbing: true
|
||||
}));
|
||||
|
||||
console.log(payload);
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/search/bulk',
|
||||
method: 'POST',
|
||||
|
||||
@@ -4,6 +4,7 @@ import createHandleActions from './Creators/createHandleActions';
|
||||
import applications from './Settings/applications';
|
||||
import appProfiles from './Settings/appProfiles';
|
||||
import development from './Settings/development';
|
||||
import downloadClientCategories from './Settings/downloadClientCategories';
|
||||
import downloadClients from './Settings/downloadClients';
|
||||
import general from './Settings/general';
|
||||
import indexerCategories from './Settings/indexerCategories';
|
||||
@@ -11,6 +12,7 @@ import indexerProxies from './Settings/indexerProxies';
|
||||
import notifications from './Settings/notifications';
|
||||
import ui from './Settings/ui';
|
||||
|
||||
export * from './Settings/downloadClientCategories';
|
||||
export * from './Settings/downloadClients';
|
||||
export * from './Settings/general';
|
||||
export * from './Settings/indexerCategories';
|
||||
@@ -32,6 +34,7 @@ export const section = 'settings';
|
||||
export const defaultState = {
|
||||
advancedSettings: false,
|
||||
|
||||
downloadClientCategories: downloadClientCategories.defaultState,
|
||||
downloadClients: downloadClients.defaultState,
|
||||
general: general.defaultState,
|
||||
indexerCategories: indexerCategories.defaultState,
|
||||
@@ -61,6 +64,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
...downloadClientCategories.actionHandlers,
|
||||
...downloadClients.actionHandlers,
|
||||
...general.actionHandlers,
|
||||
...indexerCategories.actionHandlers,
|
||||
@@ -81,6 +85,7 @@ export const reducers = createHandleActions({
|
||||
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
|
||||
},
|
||||
|
||||
...downloadClientCategories.reducers,
|
||||
...downloadClients.reducers,
|
||||
...general.reducers,
|
||||
...indexerCategories.reducers,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<subcat id="5030" name="SD"/>
|
||||
<subcat id="5060" name="Sport"/>
|
||||
<subcat id="5010" name="WEB-DL"/>
|
||||
<subcat id="5999" name="Other"/>
|
||||
</category>
|
||||
<category id="7000" name="Other">
|
||||
<subcat id="7010" name="Misc"/>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerStats;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerStatsTests
|
||||
{
|
||||
public class IndexerStatisticsServiceFixture : CoreTest<IndexerStatisticsService>
|
||||
{
|
||||
private IndexerDefinition _indexer;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_indexer = Builder<IndexerDefinition>.CreateNew().With(x => x.Id = 5).Build();
|
||||
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(o => o.All())
|
||||
.Returns(new List<IndexerDefinition> { _indexer });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_pull_stats_if_all_events_are_failures()
|
||||
{
|
||||
var history = new List<History.History>
|
||||
{
|
||||
new History.History
|
||||
{
|
||||
Date = DateTime.UtcNow.AddHours(-1),
|
||||
EventType = HistoryEventType.IndexerRss,
|
||||
Successful = false,
|
||||
Id = 8,
|
||||
IndexerId = 5,
|
||||
Data = new Dictionary<string, string> { { "source", "prowlarr" } }
|
||||
}
|
||||
};
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(o => o.Between(It.IsAny<DateTime>(), It.IsAny<DateTime>()))
|
||||
.Returns<DateTime, DateTime>((s, f) => history);
|
||||
|
||||
var statistics = Subject.IndexerStatistics(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow);
|
||||
|
||||
statistics.IndexerStatistics.Count.Should().Be(1);
|
||||
statistics.IndexerStatistics.First().AverageResponseTime.Should().Be(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 23:26:21"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-15 04:26:21"));
|
||||
torrentInfo.Size.Should().Be(935127615);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 11:04:50"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 16:04:50"));
|
||||
torrentInfo.Size.Should().Be(7085405541);
|
||||
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 00:24:49"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
|
||||
torrentInfo.Size.Should().Be(69914591044);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
@@ -70,6 +71,32 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
caps.LimitsMax.Value.Should().Be(60);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_map_different_categories()
|
||||
{
|
||||
GivenCapsResponse(_caps);
|
||||
|
||||
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||
|
||||
var bookCats = caps.Categories.MapTorznabCapsToTrackers(new int[] { NewznabStandardCategory.Books.Id });
|
||||
|
||||
bookCats.Count.Should().Be(2);
|
||||
bookCats.Should().Contain("8000");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_map_by_name_when_available()
|
||||
{
|
||||
GivenCapsResponse(_caps);
|
||||
|
||||
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||
|
||||
var bookCats = caps.Categories.MapTrackerCatToNewznab("5999");
|
||||
|
||||
bookCats.Count.Should().Be(2);
|
||||
bookCats.First().Id.Should().Be(5050);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_default_pagesize_if_missing()
|
||||
{
|
||||
|
||||
@@ -126,6 +126,12 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
lidarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !lidarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Retain user settings not-affiliated with Prowlarr
|
||||
lidarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_lidarrV1Proxy.UpdateIndexer(lidarrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +165,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +182,11 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<LidarrField>()
|
||||
};
|
||||
|
||||
lidarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
@@ -191,7 +200,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||
{
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public int? DownloadClientId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public List<LidarrField> Fields { get; set; }
|
||||
|
||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -126,6 +126,12 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
radarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !radarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Retain user settings not-affiliated with Prowlarr
|
||||
radarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_radarrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +165,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +182,11 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<RadarrField>()
|
||||
};
|
||||
|
||||
radarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public int? DownloadClientId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public List<RadarrField> Fields { get; set; }
|
||||
|
||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -126,6 +126,8 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
readarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !readarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_readarrV1Proxy.UpdateIndexer(readarrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +161,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +178,11 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<ReadarrField>()
|
||||
};
|
||||
|
||||
readarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
@@ -191,7 +196,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||
{
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -126,6 +126,13 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
sonarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !sonarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Retain user settings not-affiliated with Prowlarr
|
||||
sonarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
|
||||
sonarrIndexer.SeasonSearchMaximumSingleEpisodeAge = remoteIndexer.SeasonSearchMaximumSingleEpisodeAge;
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_sonarrV3Proxy.UpdateIndexer(sonarrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +166,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +183,11 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<SonarrField>()
|
||||
};
|
||||
|
||||
sonarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
@@ -189,7 +199,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
|
||||
return sonarrIndexer;
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public int? DownloadClientId { get; set; }
|
||||
public int? SeasonSearchMaximumSingleEpisodeAge { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public List<SonarrField> Fields { get; set; }
|
||||
|
||||
@@ -34,7 +36,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -126,6 +126,8 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
whisparrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !whisparrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_whisparrV3Proxy.UpdateIndexer(whisparrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +161,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +178,11 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<WhisparrField>()
|
||||
};
|
||||
|
||||
whisparrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(023)]
|
||||
public class download_client_categories : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("DownloadClients")
|
||||
.AddColumn("Categories").AsString().WithDefaultValue("[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,8 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
Mapper.Entity<DownloadClientDefinition>("DownloadClients").RegisterModel()
|
||||
.Ignore(x => x.ImplementationName)
|
||||
.Ignore(i => i.Protocol)
|
||||
.Ignore(d => d.SupportsCategories)
|
||||
.Ignore(d => d.Protocol)
|
||||
.Ignore(d => d.Tags);
|
||||
|
||||
Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel()
|
||||
@@ -115,6 +116,7 @@ namespace NzbDrone.Core.Datastore
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<DownloadClientCategory>>());
|
||||
SqlMapper.AddTypeHandler(new OsPathConverter());
|
||||
SqlMapper.RemoveTypeMap(typeof(Guid));
|
||||
SqlMapper.RemoveTypeMap(typeof(Guid?));
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
||||
|
||||
public override string Name => "Aria2";
|
||||
|
||||
public override bool SupportsCategories => false;
|
||||
|
||||
public Aria2(IAria2Proxy proxy,
|
||||
ITorrentFileInfoReader torrentFileInfoReader,
|
||||
IHttpClient httpClient,
|
||||
|
||||
@@ -74,6 +74,8 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||
|
||||
public override string Name => "Torrent Blackhole";
|
||||
|
||||
public override bool SupportsCategories => false;
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestFolder(Settings.TorrentFolder, "TorrentFolder"));
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||
}
|
||||
|
||||
public override string Name => "Usenet Blackhole";
|
||||
public override bool SupportsCategories => false;
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -38,9 +39,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
}
|
||||
|
||||
// _proxy.SetTorrentSeedingConfiguration(actualHash, remoteMovie.SeedConfiguration, Settings);
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
if (category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_proxy.SetTorrentLabel(actualHash, Settings.Category, Settings);
|
||||
_proxy.SetTorrentLabel(actualHash, category, Settings);
|
||||
}
|
||||
|
||||
if (Settings.Priority == (int)DelugePriority.First)
|
||||
@@ -61,9 +63,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
}
|
||||
|
||||
// _proxy.SetTorrentSeedingConfiguration(actualHash, release.SeedConfiguration, Settings);
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
if (category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_proxy.SetTorrentLabel(actualHash, Settings.Category, Settings);
|
||||
_proxy.SetTorrentLabel(actualHash, category, Settings);
|
||||
}
|
||||
|
||||
if (Settings.Priority == (int)DelugePriority.First)
|
||||
@@ -75,6 +78,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
}
|
||||
|
||||
public override string Name => "Deluge";
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
@@ -139,7 +143,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
|
||||
private ValidationFailure TestCategory()
|
||||
{
|
||||
if (Settings.Category.IsNullOrWhiteSpace())
|
||||
if (Categories.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -156,23 +160,42 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
|
||||
var labels = _proxy.GetAvailableLabels(Settings);
|
||||
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace() && !labels.Contains(Settings.Category))
|
||||
{
|
||||
_proxy.AddLabel(Settings.Category, Settings);
|
||||
labels = _proxy.GetAvailableLabels(Settings);
|
||||
var categories = Categories.Select(c => c.ClientCategory).ToList();
|
||||
categories.Add(Settings.Category);
|
||||
|
||||
if (!labels.Contains(Settings.Category))
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (category.IsNotNullOrWhiteSpace() && !labels.Contains(category))
|
||||
{
|
||||
return new NzbDroneValidationFailure("Category", "Configuration of label failed")
|
||||
_proxy.AddLabel(category, Settings);
|
||||
labels = _proxy.GetAvailableLabels(Settings);
|
||||
|
||||
if (!labels.Contains(category))
|
||||
{
|
||||
DetailedDescription = "Prowlarr was unable to add the label to Deluge."
|
||||
};
|
||||
return new NzbDroneValidationFailure("Category", "Configuration of label failed")
|
||||
{
|
||||
DetailedDescription = "Prowlarr was unable to add the label to Deluge."
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override void ValidateCategories(List<ValidationFailure> failures)
|
||||
{
|
||||
base.ValidateCategories(failures);
|
||||
|
||||
foreach (var label in Categories)
|
||||
{
|
||||
if (!Regex.IsMatch(label.ClientCategory, "^[-a-z0-9]*$"))
|
||||
{
|
||||
failures.AddIfNotNull(new ValidationFailure(string.Empty, "Mapped Categories allowed characters a-z, 0-9 and -"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ValidationFailure TestGetTorrents()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
string[] GetAvailablePlugins(DelugeSettings settings);
|
||||
string[] GetEnabledPlugins(DelugeSettings settings);
|
||||
string[] GetAvailableLabels(DelugeSettings settings);
|
||||
DelugeLabel GetLabelOptions(DelugeSettings settings);
|
||||
DelugeLabel GetLabelOptions(DelugeSettings settings, string label);
|
||||
void SetTorrentLabel(string hash, string label, DelugeSettings settings);
|
||||
void SetTorrentConfiguration(string hash, string key, object value, DelugeSettings settings);
|
||||
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, DelugeSettings settings);
|
||||
@@ -158,9 +158,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
return response;
|
||||
}
|
||||
|
||||
public DelugeLabel GetLabelOptions(DelugeSettings settings)
|
||||
public DelugeLabel GetLabelOptions(DelugeSettings settings, string label)
|
||||
{
|
||||
var response = ProcessRequest<DelugeLabel>(settings, "label.get_options", settings.Category);
|
||||
var response = ProcessRequest<DelugeLabel>(settings, "label.get_options", label);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
[FieldDefinition(5, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback Category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing items")]
|
||||
|
||||
@@ -18,9 +18,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Cannot start with /");
|
||||
|
||||
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
|
||||
RuleFor(c => c.Category).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
|
||||
|
||||
RuleFor(c => c.TvCategory).Empty()
|
||||
RuleFor(c => c.Category).Empty()
|
||||
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Cannot use Category and Directory");
|
||||
}
|
||||
@@ -45,8 +45,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
|
||||
public string TvCategory { get; set; }
|
||||
[FieldDefinition(5, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")]
|
||||
public string TvDirectory { get; set; }
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
}
|
||||
|
||||
public override string Name => "Download Station";
|
||||
public override bool SupportsCategories => false;
|
||||
|
||||
public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning);
|
||||
|
||||
@@ -198,7 +199,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
if (downloadDir != null)
|
||||
{
|
||||
var sharedFolder = downloadDir.Split('\\', '/')[0];
|
||||
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
|
||||
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.Category);
|
||||
|
||||
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
|
||||
|
||||
@@ -311,11 +312,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
return Settings.TvDirectory.TrimStart('/');
|
||||
}
|
||||
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
else if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var destDir = GetDefaultDir();
|
||||
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.Category}";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
}
|
||||
|
||||
public override string Name => "Download Station";
|
||||
public override bool SupportsCategories => false;
|
||||
|
||||
public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning);
|
||||
|
||||
@@ -101,7 +102,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
if (downloadDir != null)
|
||||
{
|
||||
var sharedFolder = downloadDir.Split('\\', '/')[0];
|
||||
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
|
||||
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.Category);
|
||||
|
||||
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
|
||||
|
||||
@@ -272,11 +273,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
return Settings.TvDirectory.TrimStart('/');
|
||||
}
|
||||
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
else if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var destDir = GetDefaultDir();
|
||||
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.Category}";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> HandleTags(ReleaseInfo release, FloodSettings settings)
|
||||
private static IEnumerable<string> HandleTags(ReleaseInfo release, FloodSettings settings, string mappedCategory)
|
||||
{
|
||||
var result = new HashSet<string>();
|
||||
|
||||
@@ -36,6 +36,11 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
||||
result.UnionWith(settings.Tags);
|
||||
}
|
||||
|
||||
if (mappedCategory != null)
|
||||
{
|
||||
result.Add(mappedCategory);
|
||||
}
|
||||
|
||||
if (settings.AdditionalTags.Any())
|
||||
{
|
||||
foreach (var additionalTag in settings.AdditionalTags)
|
||||
@@ -55,18 +60,19 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
||||
}
|
||||
|
||||
public override string Name => "Flood";
|
||||
public override bool SupportsCategories => true;
|
||||
public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to remove torrents that have finished seeding when using Flood", ProviderMessageType.Warning);
|
||||
|
||||
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
|
||||
{
|
||||
_proxy.AddTorrentByFile(Convert.ToBase64String(fileContent), HandleTags(release, Settings), Settings);
|
||||
_proxy.AddTorrentByFile(Convert.ToBase64String(fileContent), HandleTags(release, Settings, GetCategoryForRelease(release)), Settings);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
|
||||
{
|
||||
_proxy.AddTorrentByUrl(magnetLink, HandleTags(release, Settings), Settings);
|
||||
_proxy.AddTorrentByUrl(magnetLink, HandleTags(release, Settings, GetCategoryForRelease(release)), Settings);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
||||
}
|
||||
|
||||
public override string Name => "Hadouken";
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
@@ -41,14 +42,14 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
||||
|
||||
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
|
||||
{
|
||||
_proxy.AddTorrentUri(Settings, magnetLink);
|
||||
_proxy.AddTorrentUri(Settings, magnetLink, GetCategoryForRelease(release) ?? Settings.Category);
|
||||
|
||||
return hash.ToUpper();
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
|
||||
{
|
||||
return _proxy.AddTorrentFile(Settings, fileContent).ToUpper();
|
||||
return _proxy.AddTorrentFile(Settings, fileContent, GetCategoryForRelease(release) ?? Settings.Category).ToUpper();
|
||||
}
|
||||
|
||||
private ValidationFailure TestConnection()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
@@ -13,8 +13,8 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
||||
HadoukenSystemInfo GetSystemInfo(HadoukenSettings settings);
|
||||
HadoukenTorrent[] GetTorrents(HadoukenSettings settings);
|
||||
IReadOnlyDictionary<string, object> GetConfig(HadoukenSettings settings);
|
||||
string AddTorrentFile(HadoukenSettings settings, byte[] fileContent);
|
||||
void AddTorrentUri(HadoukenSettings settings, string torrentUrl);
|
||||
string AddTorrentFile(HadoukenSettings settings, byte[] fileContent, string label);
|
||||
void AddTorrentUri(HadoukenSettings settings, string torrentUrl, string label);
|
||||
void RemoveTorrent(HadoukenSettings settings, string downloadId);
|
||||
void RemoveTorrentAndData(HadoukenSettings settings, string downloadId);
|
||||
}
|
||||
@@ -47,14 +47,14 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
||||
return ProcessRequest<IReadOnlyDictionary<string, object>>(settings, "webui.getSettings");
|
||||
}
|
||||
|
||||
public string AddTorrentFile(HadoukenSettings settings, byte[] fileContent)
|
||||
public string AddTorrentFile(HadoukenSettings settings, byte[] fileContent, string label)
|
||||
{
|
||||
return ProcessRequest<string>(settings, "webui.addTorrent", "file", Convert.ToBase64String(fileContent), new { label = settings.Category });
|
||||
return ProcessRequest<string>(settings, "webui.addTorrent", "file", Convert.ToBase64String(fileContent), new { label });
|
||||
}
|
||||
|
||||
public void AddTorrentUri(HadoukenSettings settings, string torrentUrl)
|
||||
public void AddTorrentUri(HadoukenSettings settings, string torrentUrl, string label)
|
||||
{
|
||||
ProcessRequest<string>(settings, "webui.addTorrent", "url", torrentUrl, new { label = settings.Category });
|
||||
ProcessRequest<string>(settings, "webui.addTorrent", "url", torrentUrl, new { label });
|
||||
}
|
||||
|
||||
public void RemoveTorrent(HadoukenSettings settings, string downloadId)
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
||||
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox)]
|
||||
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release.")]
|
||||
public string Category { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -29,8 +29,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContents)
|
||||
{
|
||||
var priority = Settings.Priority;
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
|
||||
var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings);
|
||||
var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings, category);
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
@@ -41,6 +42,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
}
|
||||
|
||||
public override string Name => "NZBVortex";
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
protected List<NzbVortexGroup> GetGroups()
|
||||
{
|
||||
@@ -111,19 +113,27 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
|
||||
private ValidationFailure TestCategory()
|
||||
{
|
||||
var group = GetGroups().FirstOrDefault(c => c.GroupName == Settings.Category);
|
||||
var groups = GetGroups();
|
||||
|
||||
if (group == null)
|
||||
foreach (var category in Categories)
|
||||
{
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
if (!category.ClientCategory.IsNullOrWhiteSpace() && !groups.Any(v => v.GroupName == category.ClientCategory))
|
||||
{
|
||||
return new NzbDroneValidationFailure("Category", "Group does not exist")
|
||||
return new NzbDroneValidationFailure(string.Empty, "Group does not exist")
|
||||
{
|
||||
DetailedDescription = "The Group you entered doesn't exist in NzbVortex. Go to NzbVortex to create it."
|
||||
DetailedDescription = "A mapped category you entered doesn't exist in NzbVortex. Go to NzbVortex to create it."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!Settings.Category.IsNullOrWhiteSpace() && !groups.Any(v => v.GroupName == Settings.Category))
|
||||
{
|
||||
return new NzbDroneValidationFailure("Category", "Category does not exist")
|
||||
{
|
||||
DetailedDescription = "The category you entered doesn't exist in NzbVortex. Go to NzbVortex to create it."
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
{
|
||||
public interface INzbVortexProxy
|
||||
{
|
||||
string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings);
|
||||
string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings, string group);
|
||||
void Remove(int id, bool deleteData, NzbVortexSettings settings);
|
||||
NzbVortexVersionResponse GetVersion(NzbVortexSettings settings);
|
||||
NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings);
|
||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
_authSessionIdCache = cacheManager.GetCache<string>(GetType(), "authCache");
|
||||
}
|
||||
|
||||
public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings)
|
||||
public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings, string group)
|
||||
{
|
||||
var requestBuilder = BuildRequest(settings).Resource("nzb/add")
|
||||
.Post()
|
||||
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
|
||||
if (settings.Category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
requestBuilder.AddQueryParam("groupname", settings.Category);
|
||||
requestBuilder.AddQueryParam("groupname", group);
|
||||
}
|
||||
|
||||
requestBuilder.AddFormUpload("name", filename, nzbData, "application/x-nzb");
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
[FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Group", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
[FieldDefinition(4, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing items")]
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
|
||||
protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContent)
|
||||
{
|
||||
var category = Settings.Category;
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
|
||||
var priority = Settings.Priority;
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
|
||||
protected override string AddFromLink(ReleaseInfo release)
|
||||
{
|
||||
var category = Settings.Category;
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
|
||||
var priority = Settings.Priority;
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
}
|
||||
|
||||
public override string Name => "NZBGet";
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
protected IEnumerable<NzbgetCategory> GetCategories(Dictionary<string, string> config)
|
||||
{
|
||||
@@ -139,6 +140,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var categories = GetCategories(config);
|
||||
|
||||
foreach (var category in Categories)
|
||||
{
|
||||
if (!category.ClientCategory.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == category.ClientCategory))
|
||||
{
|
||||
return new NzbDroneValidationFailure(string.Empty, "Category does not exist")
|
||||
{
|
||||
InfoLink = _proxy.GetBaseUrl(Settings),
|
||||
DetailedDescription = "A mapped category you entered doesn't exist in NZBGet. Go to NZBGet to create it."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!Settings.Category.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == Settings.Category))
|
||||
{
|
||||
return new NzbDroneValidationFailure("Category", "Category does not exist")
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority for items added from Prowlarr")]
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
|
||||
}
|
||||
|
||||
public override string Name => "Pneumatic";
|
||||
public override bool SupportsCategories => false;
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||
|
||||
|
||||
@@ -52,8 +52,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
//var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
|
||||
var itemToTop = Settings.Priority == (int)QBittorrentPriority.First;
|
||||
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
|
||||
Proxy.AddTorrentFromUrl(magnetLink, null, Settings);
|
||||
Proxy.AddTorrentFromUrl(magnetLink, null, Settings, category);
|
||||
|
||||
if (itemToTop || forceStart)
|
||||
{
|
||||
@@ -100,8 +101,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
//var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
|
||||
var itemToTop = Settings.Priority == (int)QBittorrentPriority.First;
|
||||
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
|
||||
Proxy.AddTorrentFromFile(filename, fileContent, null, Settings);
|
||||
Proxy.AddTorrentFromFile(filename, fileContent, null, Settings, category);
|
||||
|
||||
if (itemToTop || forceStart)
|
||||
{
|
||||
@@ -167,6 +169,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
|
||||
public override string Name => "qBittorrent";
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
@@ -197,7 +200,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
else if (version < Version.Parse("1.6"))
|
||||
{
|
||||
// API version 6 introduced support for labels
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace() || Categories.Count > 0)
|
||||
{
|
||||
return new NzbDroneValidationFailure("Category", "Category is not supported")
|
||||
{
|
||||
@@ -205,15 +208,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (Settings.Category.IsNullOrWhiteSpace())
|
||||
{
|
||||
// warn if labels are supported, but category is not provided
|
||||
return new NzbDroneValidationFailure("Category", "Category is recommended")
|
||||
{
|
||||
IsWarning = true,
|
||||
DetailedDescription = "Prowlarr will not attempt to import completed downloads without a category."
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (DownloadClientAuthenticationException ex)
|
||||
{
|
||||
@@ -251,7 +245,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
|
||||
private ValidationFailure TestCategory()
|
||||
{
|
||||
if (Settings.Category.IsNullOrWhiteSpace())
|
||||
if (Settings.Category.IsNullOrWhiteSpace() && Categories.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -265,6 +259,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
|
||||
Dictionary<string, QBittorrentLabel> labels = Proxy.GetLabels(Settings);
|
||||
|
||||
foreach (var category in Categories)
|
||||
{
|
||||
if (category.ClientCategory.IsNotNullOrWhiteSpace() && !labels.ContainsKey(category.ClientCategory))
|
||||
{
|
||||
Proxy.AddLabel(category.ClientCategory, Settings);
|
||||
labels = Proxy.GetLabels(Settings);
|
||||
|
||||
if (!labels.ContainsKey(category.ClientCategory))
|
||||
{
|
||||
return new NzbDroneValidationFailure(string.Empty, "Configuration of label failed")
|
||||
{
|
||||
DetailedDescription = "Prowlarr was unable to add the label to qBittorrent."
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace() && !labels.ContainsKey(Settings.Category))
|
||||
{
|
||||
Proxy.AddLabel(Settings.Category, Settings);
|
||||
|
||||
@@ -18,8 +18,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings);
|
||||
List<QBittorrentTorrentFile> GetTorrentFiles(string hash, QBittorrentSettings settings);
|
||||
|
||||
void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
|
||||
void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
|
||||
void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category);
|
||||
void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category);
|
||||
|
||||
void RemoveTorrent(string hash, bool removeData, QBittorrentSettings settings);
|
||||
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
|
||||
|
||||
@@ -113,15 +113,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
return response;
|
||||
}
|
||||
|
||||
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
||||
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/download")
|
||||
.Post()
|
||||
.AddFormParameter("urls", torrentUrl);
|
||||
|
||||
if (settings.Category.IsNotNullOrWhiteSpace())
|
||||
if (category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("category", settings.Category);
|
||||
request.AddFormParameter("category", category);
|
||||
}
|
||||
|
||||
// Note: ForceStart is handled by separate api call
|
||||
@@ -143,15 +143,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
||||
public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/upload")
|
||||
.Post()
|
||||
.AddFormUpload("torrents", fileName, fileContent);
|
||||
|
||||
if (settings.Category.IsNotNullOrWhiteSpace())
|
||||
if (category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("category", settings.Category);
|
||||
request.AddFormParameter("category", category);
|
||||
}
|
||||
|
||||
// Note: ForceStart is handled by separate api call
|
||||
|
||||
@@ -119,14 +119,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
return response;
|
||||
}
|
||||
|
||||
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
||||
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
|
||||
.Post()
|
||||
.AddFormParameter("urls", torrentUrl);
|
||||
if (settings.Category.IsNotNullOrWhiteSpace())
|
||||
if (category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("category", settings.Category);
|
||||
request.AddFormParameter("category", category);
|
||||
}
|
||||
|
||||
// Note: ForceStart is handled by separate api call
|
||||
@@ -153,15 +153,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
||||
public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
|
||||
.Post()
|
||||
.AddFormUpload("torrents", fileName, fileContent);
|
||||
|
||||
if (settings.Category.IsNotNullOrWhiteSpace())
|
||||
if (category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("category", settings.Category);
|
||||
request.AddFormParameter("category", category);
|
||||
}
|
||||
|
||||
// Note: ForceStart is handled by separate api call
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing items")]
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
|
||||
protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContent)
|
||||
{
|
||||
var category = Settings.Category;
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
var priority = Settings.Priority;
|
||||
|
||||
var response = _proxy.DownloadNzb(fileContent, filename, category, priority, Settings);
|
||||
@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
|
||||
protected override string AddFromLink(ReleaseInfo release)
|
||||
{
|
||||
var category = Settings.Category;
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
var priority = Settings.Priority;
|
||||
|
||||
var response = _proxy.DownloadNzbByUrl(release.DownloadUrl, category, priority, Settings);
|
||||
@@ -62,6 +62,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
}
|
||||
|
||||
public override string Name => "SABnzbd";
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
protected IEnumerable<SabnzbdCategory> GetCategories(SabnzbdConfig config)
|
||||
{
|
||||
@@ -260,29 +261,27 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
private ValidationFailure TestCategory()
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var category = GetCategories(config).FirstOrDefault((SabnzbdCategory v) => v.Name == Settings.Category);
|
||||
var categories = GetCategories(config);
|
||||
|
||||
if (category != null)
|
||||
foreach (var category in Categories)
|
||||
{
|
||||
if (category.Dir.EndsWith("*"))
|
||||
if (!category.ClientCategory.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == category.ClientCategory))
|
||||
{
|
||||
return new NzbDroneValidationFailure("Category", "Enable Job folders")
|
||||
return new NzbDroneValidationFailure(string.Empty, "Category does not exist")
|
||||
{
|
||||
InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"),
|
||||
DetailedDescription = "Prowlarr prefers each download to have a separate folder. With * appended to the Folder/Path SABnzbd will not create these job folders. Go to SABnzbd to fix it."
|
||||
InfoLink = _proxy.GetBaseUrl(Settings),
|
||||
DetailedDescription = "A mapped category you entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it."
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (!Settings.Category.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == Settings.Category))
|
||||
{
|
||||
if (!Settings.Category.IsNullOrWhiteSpace())
|
||||
return new NzbDroneValidationFailure("Category", "Category does not exist")
|
||||
{
|
||||
return new NzbDroneValidationFailure("Category", "Category does not exist")
|
||||
{
|
||||
InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"),
|
||||
DetailedDescription = "The category you entered doesn't exist in SABnzbd. Go to SABnzbd to create it."
|
||||
};
|
||||
}
|
||||
InfoLink = _proxy.GetBaseUrl(Settings),
|
||||
DetailedDescription = "The category you entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it."
|
||||
};
|
||||
}
|
||||
|
||||
if (config.Misc.enable_tv_sorting && ContainsCategory(config.Misc.tv_categories, Settings.Category))
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
[FieldDefinition(6, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
[FieldDefinition(7, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing items")]
|
||||
|
||||
@@ -38,5 +38,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
}
|
||||
|
||||
public override string Name => "Transmission";
|
||||
public override bool SupportsCategories => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
|
||||
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")]
|
||||
|
||||
@@ -58,5 +58,6 @@ namespace NzbDrone.Core.Download.Clients.Vuze
|
||||
}
|
||||
|
||||
public override string Name => "Vuze";
|
||||
public override bool SupportsCategories => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
{
|
||||
var priority = (RTorrentPriority)Settings.Priority;
|
||||
|
||||
_proxy.AddTorrentFromUrl(magnetLink, Settings.Category, priority, Settings.Directory, Settings);
|
||||
_proxy.AddTorrentFromUrl(magnetLink, GetCategoryForRelease(release) ?? Settings.Category, priority, Settings.Directory, Settings);
|
||||
|
||||
var tries = 10;
|
||||
var retryDelay = 500;
|
||||
@@ -58,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
{
|
||||
var priority = (RTorrentPriority)Settings.Priority;
|
||||
|
||||
_proxy.AddTorrentFromFile(filename, fileContent, Settings.Category, priority, Settings.Directory, Settings);
|
||||
_proxy.AddTorrentFromFile(filename, fileContent, GetCategoryForRelease(release) ?? Settings.Category, priority, Settings.Directory, Settings);
|
||||
|
||||
var tries = 10;
|
||||
var retryDelay = 500;
|
||||
@@ -73,6 +73,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
}
|
||||
|
||||
public override string Name => "rTorrent";
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to remove torrents that have finished seeding when using rTorrent", ProviderMessageType.Warning);
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional.")]
|
||||
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional.")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")]
|
||||
|
||||
@@ -38,9 +38,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
_proxy.AddTorrentFromUrl(magnetLink, Settings);
|
||||
|
||||
//_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
if (GetCategoryForRelease(release).IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_proxy.SetTorrentLabel(hash, Settings.Category, Settings);
|
||||
_proxy.SetTorrentLabel(hash, category, Settings);
|
||||
}
|
||||
|
||||
if (Settings.Priority == (int)UTorrentPriority.First)
|
||||
@@ -58,9 +59,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
|
||||
|
||||
//_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings);
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
if (category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_proxy.SetTorrentLabel(hash, Settings.Category, Settings);
|
||||
_proxy.SetTorrentLabel(hash, category, Settings);
|
||||
}
|
||||
|
||||
if (Settings.Priority == (int)UTorrentPriority.First)
|
||||
@@ -74,40 +76,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
}
|
||||
|
||||
public override string Name => "uTorrent";
|
||||
|
||||
private List<UTorrentTorrent> GetTorrents()
|
||||
{
|
||||
List<UTorrentTorrent> torrents;
|
||||
|
||||
var cacheKey = string.Format("{0}:{1}:{2}", Settings.Host, Settings.Port, Settings.Category);
|
||||
var cache = _torrentCache.Find(cacheKey);
|
||||
|
||||
var response = _proxy.GetTorrents(cache == null ? null : cache.CacheID, Settings);
|
||||
|
||||
if (cache != null && response.Torrents == null)
|
||||
{
|
||||
var removedAndUpdated = new HashSet<string>(response.TorrentsChanged.Select(v => v.Hash).Concat(response.TorrentsRemoved));
|
||||
|
||||
torrents = cache.Torrents
|
||||
.Where(v => !removedAndUpdated.Contains(v.Hash))
|
||||
.Concat(response.TorrentsChanged)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
torrents = response.Torrents;
|
||||
}
|
||||
|
||||
cache = new UTorrentTorrentCache
|
||||
{
|
||||
CacheID = response.CacheNumber,
|
||||
Torrents = torrents
|
||||
};
|
||||
|
||||
_torrentCache.Set(cacheKey, cache, TimeSpan.FromMinutes(15));
|
||||
|
||||
return torrents;
|
||||
}
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
[FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing items")]
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
using Org.BouncyCastle.Crypto.Tls;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
@@ -50,6 +53,9 @@ namespace NzbDrone.Core.Download
|
||||
return GetType().Name;
|
||||
}
|
||||
|
||||
protected List<DownloadClientCategory> Categories => ((DownloadClientDefinition)Definition).Categories;
|
||||
public abstract bool SupportsCategories { get; }
|
||||
|
||||
public abstract DownloadProtocol Protocol
|
||||
{
|
||||
get;
|
||||
@@ -57,12 +63,54 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
public abstract Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer);
|
||||
|
||||
protected string GetCategoryForRelease(ReleaseInfo release)
|
||||
{
|
||||
var categories = ((DownloadClientDefinition)Definition).Categories;
|
||||
if (categories.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for direct mapping
|
||||
var category = categories.FirstOrDefault(x => x.Categories.Intersect(release.Categories.Select(c => c.Id)).Any())?.ClientCategory;
|
||||
|
||||
// Check for parent mapping
|
||||
if (category == null)
|
||||
{
|
||||
foreach (var cat in categories)
|
||||
{
|
||||
var mappedCat = NewznabStandardCategory.AllCats.Where(x => cat.Categories.Contains(x.Id));
|
||||
var subCats = mappedCat.SelectMany(x => x.SubCategories);
|
||||
|
||||
if (subCats.Intersect(release.Categories).Any())
|
||||
{
|
||||
category = cat.ClientCategory;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
protected virtual void ValidateCategories(List<ValidationFailure> failures)
|
||||
{
|
||||
foreach (var category in ((DownloadClientDefinition)Definition).Categories)
|
||||
{
|
||||
if (category.ClientCategory.IsNullOrWhiteSpace())
|
||||
{
|
||||
failures.AddIfNotNull(new ValidationFailure(string.Empty, "Category can not be empty"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ValidationResult Test()
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
try
|
||||
{
|
||||
ValidateCategories(failures);
|
||||
Test(failures);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -97,5 +145,17 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool HasConcreteImplementation(string methodName)
|
||||
{
|
||||
var method = GetType().GetMethod(methodName);
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
throw new MissingMethodException(GetType().Name, Name);
|
||||
}
|
||||
|
||||
return !method.DeclaringType.IsAbstract;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
src/NzbDrone.Core/Download/DownloadClientCategory.cs
Normal file
14
src/NzbDrone.Core/Download/DownloadClientCategory.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class DownloadClientCategory
|
||||
{
|
||||
public string ClientCategory { get; set; }
|
||||
public List<int> Categories { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
using NzbDrone.Core.Indexers;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class DownloadClientDefinition : ProviderDefinition
|
||||
{
|
||||
public DownloadClientDefinition()
|
||||
{
|
||||
Categories = new List<DownloadClientCategory>();
|
||||
}
|
||||
|
||||
public List<DownloadClientCategory> Categories { get; set; }
|
||||
public bool SupportsCategories { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public int Priority { get; set; } = 1;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace NzbDrone.Core.Download
|
||||
base.SetProviderCharacteristics(provider, definition);
|
||||
|
||||
definition.Protocol = provider.Protocol;
|
||||
definition.SupportsCategories = provider.SupportsCategories;
|
||||
}
|
||||
|
||||
public List<IDownloadClient> DownloadHandlingEnabled(bool filterBlockedClients = true)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -7,6 +8,7 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
public interface IDownloadClient : IProvider
|
||||
{
|
||||
bool SupportsCategories { get; }
|
||||
DownloadProtocol Protocol { get; }
|
||||
Task<string> Download(ReleaseInfo release, bool redirect, IIndexer indexer);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
|
||||
public class IndexerCheck : HealthCheckBase
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
|
||||
public class IndexerLongTermStatusCheck : HealthCheckBase
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
|
||||
public class IndexerStatusCheck : HealthCheckBase
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class IndexerVIPCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class IndexerVIPExpiredCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
@@ -9,6 +9,7 @@ using NzbDrone.Core.ThingiProvider.Events;
|
||||
namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class NoDefinitionCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;
|
||||
|
||||
@@ -9,6 +9,7 @@ using NzbDrone.Core.ThingiProvider.Events;
|
||||
namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class OutdatedDefinitionCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;
|
||||
|
||||
@@ -56,9 +56,10 @@ namespace NzbDrone.Core.IndexerStats
|
||||
.ToArray();
|
||||
int temp = 0;
|
||||
|
||||
indexerStats.AverageResponseTime = (int)sortedEvents.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp))
|
||||
.Select(h => temp)
|
||||
.Average();
|
||||
var elapsedTimeEvents = sortedEvents.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp))
|
||||
.Select(h => temp);
|
||||
|
||||
indexerStats.AverageResponseTime = elapsedTimeEvents.Count() > 0 ? (int)elapsedTimeEvents.Average() : 0;
|
||||
|
||||
foreach (var historyEvent in sortedEvents)
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
public AvistazIdInfo MovieTvinfo { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "file_name")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
InfoUrl = details,
|
||||
Guid = details,
|
||||
Categories = cats,
|
||||
PublishDate = row.CreatedAt,
|
||||
PublishDate = DateTime.Parse(row.CreatedAt + "-05:00").ToUniversalTime(), // Avistaz does not specify a timezone & returns server time
|
||||
Size = row.FileSize,
|
||||
Files = row.FileCount,
|
||||
Grabs = row.Completed,
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
public class CardigannSettingsValidator : AbstractValidator<CardigannSettings>
|
||||
{
|
||||
public CardigannSettingsValidator()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class CardigannSettings : NoAuthTorrentBaseSettings
|
||||
{
|
||||
private static readonly CardigannSettingsValidator Validator = new CardigannSettingsValidator();
|
||||
|
||||
public CardigannSettings()
|
||||
{
|
||||
ExtraFieldData = new Dictionary<string, object>();
|
||||
@@ -26,10 +18,5 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public string DefinitionFile { get; set; }
|
||||
|
||||
public Dictionary<string, object> ExtraFieldData { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,11 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||
|
||||
var baseUrl = string.Format("{0}/api.php?action={1}&category={2}&username={3}&passkey={4}{5}", Settings.BaseUrl.TrimEnd('/'), searchType, categoriesQuery, Settings.Username.Trim(), Settings.Passkey.Trim(), parameters);
|
||||
|
||||
if (Settings.FreeleechOnly)
|
||||
{
|
||||
baseUrl += "&freeleech=1";
|
||||
}
|
||||
|
||||
yield return new IndexerRequest(baseUrl, HttpAccept.Json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||
[FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey (This is the alphanumeric string in the tracker url shown in your download client)", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Passkey { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Freeleech Only", HelpText = "Search Freeleech torrents only", Type = FieldType.Checkbox)]
|
||||
public bool FreeleechOnly { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace NzbDrone.Core.Indexers.Headphones
|
||||
{
|
||||
RuleFor(c => c.Username).NotEmpty();
|
||||
RuleFor(c => c.Password).NotEmpty();
|
||||
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
@@ -25,7 +21,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public override string Name => "Nebulance";
|
||||
public override string[] IndexerUrls => new string[] { "https://nebulance.io/" };
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override string Description => "Nebulance (NBL) is a ratioless Private Torrent Tracker for TV";
|
||||
public override string Language => "en-US";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
@@ -48,53 +43,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return new NebulanceParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true
|
||||
};
|
||||
|
||||
requestBuilder.Method = HttpMethod.Post;
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var cookies = Cookies;
|
||||
|
||||
Cookies = null;
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("twofa", Settings.TwoFactorAuth)
|
||||
.AddFormParameter("keeplogged", "on")
|
||||
.AddFormParameter("login", "Login")
|
||||
.SetHeader("Content-Type", "multipart/form-data")
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
|
||||
_logger.Debug("Nebulance authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (!httpResponse.Content.Contains("logout.php"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,30 +70,16 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(NebulanceQuery parameters, int? results, int? offset)
|
||||
{
|
||||
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
|
||||
var apiUrl = Settings.BaseUrl + "api.php";
|
||||
|
||||
var searchTerm = term;
|
||||
var builder = new JsonRpcRequestBuilder(apiUrl)
|
||||
.Call("getTorrents", Settings.ApiKey, parameters, results ?? 100, offset ?? 0);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
searchTerm = Regex.Replace(searchTerm, @"[-._]", " ");
|
||||
}
|
||||
builder.SuppressHttpError = true;
|
||||
|
||||
var qc = new NameValueCollection
|
||||
{
|
||||
{ "action", "basic" },
|
||||
{ "order_by", "time" },
|
||||
{ "order_way", "desc" },
|
||||
{ "searchtext", searchTerm }
|
||||
};
|
||||
|
||||
searchUrl = searchUrl + "?" + qc.GetQueryString();
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
yield return request;
|
||||
yield return new IndexerRequest(builder.Build());
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
@@ -159,7 +100,27 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
var queryParams = new NebulanceQuery
|
||||
{
|
||||
Age = ">0"
|
||||
};
|
||||
|
||||
if (searchCriteria.SanitizedTvSearchString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Name = "%" + searchCriteria.SanitizedTvSearchString + "%";
|
||||
}
|
||||
|
||||
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.ImdbId, out var intImdb))
|
||||
{
|
||||
queryParams.Imdb = intImdb;
|
||||
|
||||
if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Name = "%" + searchCriteria.EpisodeSearchString + "%";
|
||||
}
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -175,7 +136,17 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
var queryParams = new NebulanceQuery
|
||||
{
|
||||
Age = ">0"
|
||||
};
|
||||
|
||||
if (searchCriteria.SanitizedSearchTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Name = "%" + searchCriteria.SanitizedSearchTerm + "%";
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -199,60 +170,38 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var document = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = document.QuerySelectorAll(".torrent_table > tbody > tr[class^='torrent row']");
|
||||
JsonRpcResponse<NebulanceTorrents> jsonResponse = new HttpResponse<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse).Resource;
|
||||
|
||||
if (jsonResponse.Error != null || jsonResponse.Result == null)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error);
|
||||
}
|
||||
|
||||
if (jsonResponse.Result.Items.Count == 0)
|
||||
{
|
||||
return torrentInfos;
|
||||
}
|
||||
|
||||
var rows = jsonResponse.Result.Items;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var title = row.QuerySelector("a[data-src]").GetAttribute("data-src");
|
||||
if (string.IsNullOrEmpty(title) || title == "0")
|
||||
{
|
||||
title = row.QuerySelector("a[data-src]").TextContent;
|
||||
title = Regex.Replace(title, @"[\[\]\/]", "");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (title.Length > 5 && title.Substring(title.Length - 5).Contains("."))
|
||||
{
|
||||
title = title.Remove(title.LastIndexOf(".", StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
var posterStr = row.QuerySelector("img")?.GetAttribute("src");
|
||||
Uri.TryCreate(posterStr, UriKind.Absolute, out var poster);
|
||||
|
||||
var details = _settings.BaseUrl + row.QuerySelector("a[data-src]").GetAttribute("href");
|
||||
var link = _settings.BaseUrl + row.QuerySelector("a[href*='action=download']").GetAttribute("href");
|
||||
|
||||
var qColSize = row.QuerySelector("td:nth-child(3)");
|
||||
var size = ParseUtil.GetBytes(qColSize.Children[0].TextContent);
|
||||
var files = ParseUtil.CoerceInt(qColSize.Children[1].TextContent.Split(':')[1].Trim());
|
||||
|
||||
var qPublishdate = row.QuerySelector("td:nth-child(4) span");
|
||||
var publishDateStr = qPublishdate.GetAttribute("title");
|
||||
var publishDate = !string.IsNullOrEmpty(publishDateStr) && publishDateStr.Contains(",")
|
||||
? DateTime.ParseExact(publishDateStr, "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture)
|
||||
: DateTime.ParseExact(qPublishdate.TextContent.Trim(), "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture);
|
||||
|
||||
var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(5)").TextContent);
|
||||
var seeds = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent);
|
||||
var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent);
|
||||
var details = _settings.BaseUrl + "torrents.php?id=" + row.TorrentId;
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = title,
|
||||
Title = row.ReleaseTitle,
|
||||
Guid = details,
|
||||
InfoUrl = details,
|
||||
PosterUrl = poster?.AbsoluteUri ?? null,
|
||||
DownloadUrl = link,
|
||||
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(title) },
|
||||
Size = size,
|
||||
Files = files,
|
||||
PublishDate = publishDate,
|
||||
Grabs = grabs,
|
||||
Seeders = seeds,
|
||||
Peers = seeds + leechers,
|
||||
PosterUrl = row.Banner,
|
||||
DownloadUrl = row.Download,
|
||||
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) },
|
||||
Size = ParseUtil.CoerceLong(row.Size),
|
||||
Files = row.FileList.Length,
|
||||
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal),
|
||||
Grabs = ParseUtil.CoerceInt(row.Snatch),
|
||||
Seeders = ParseUtil.CoerceInt(row.Seed),
|
||||
Peers = ParseUtil.CoerceInt(row.Seed) + ParseUtil.CoerceInt(row.Leech),
|
||||
MinimumRatio = 0, // ratioless
|
||||
MinimumSeedTime = 86400, // 24 hours
|
||||
DownloadVolumeFactor = 0, // ratioless tracker
|
||||
@@ -268,14 +217,69 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class NebulanceSettings : UserPassTorrentBaseSettings
|
||||
public class NebulanceSettings : NoAuthTorrentBaseSettings
|
||||
{
|
||||
public NebulanceSettings()
|
||||
{
|
||||
TwoFactorAuth = "";
|
||||
ApiKey = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(4, Label = "Two Factor Auth", HelpText = "Two-Factor Auth")]
|
||||
public string TwoFactorAuth { get; set; }
|
||||
[FieldDefinition(4, Label = "API Key", HelpText = "API Key from User Settings > Api Keys. Key must have List and Download permissions")]
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
|
||||
public class NebulanceQuery
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Id { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Time { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Age { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int Tvmaze { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int Imdb { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Hash { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string[] Tags { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Category { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Series { get; set; }
|
||||
|
||||
public NebulanceQuery Clone()
|
||||
{
|
||||
return MemberwiseClone() as NebulanceQuery;
|
||||
}
|
||||
}
|
||||
|
||||
public class NebulanceTorrent
|
||||
{
|
||||
[JsonProperty(PropertyName = "rls_name")]
|
||||
public string ReleaseTitle { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string Seed { get; set; }
|
||||
public string Leech { get; set; }
|
||||
public string Snatch { get; set; }
|
||||
public string Download { get; set; }
|
||||
[JsonProperty(PropertyName = "file_list")]
|
||||
public string[] FileList { get; set; }
|
||||
[JsonProperty(PropertyName = "series_banner")]
|
||||
public string Banner { get; set; }
|
||||
[JsonProperty(PropertyName = "group_id")]
|
||||
public string TorrentId { get; set; }
|
||||
[JsonProperty(PropertyName = "rls_utc")]
|
||||
public string PublishDateUtc { get; set; }
|
||||
}
|
||||
|
||||
public class NebulanceTorrents
|
||||
{
|
||||
public List<NebulanceTorrent> Items { get; set; }
|
||||
public int Results { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new NewznabRssParser(Settings);
|
||||
return new NewznabRssParser(Settings, Definition, _capabilitiesProvider);
|
||||
}
|
||||
|
||||
public string[] GetBaseUrlFromSettings()
|
||||
@@ -181,13 +181,13 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
|
||||
if (capabilities.MovieSearchParams != null &&
|
||||
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
|
||||
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId, MovieSearchParam.TraktId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (capabilities.TvSearchParams != null &&
|
||||
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
|
||||
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.ImdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
|
||||
new[] { TvSearchParam.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
@@ -221,27 +222,59 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
foreach (var xmlCategory in xmlCategories.Elements("category"))
|
||||
{
|
||||
var cat = new IndexerCategory
|
||||
var parentName = xmlCategory.Attribute("name").Value;
|
||||
var parentId = int.Parse(xmlCategory.Attribute("id").Value);
|
||||
|
||||
var mappedCat = NewznabStandardCategory.ParentCats.FirstOrDefault(x => parentName.ToLower().Contains(x.Name.ToLower()));
|
||||
|
||||
if (mappedCat == null)
|
||||
{
|
||||
Id = int.Parse(xmlCategory.Attribute("id").Value),
|
||||
Name = xmlCategory.Attribute("name").Value,
|
||||
Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty
|
||||
};
|
||||
// Try by parent id if name fails
|
||||
mappedCat = NewznabStandardCategory.ParentCats.FirstOrDefault(x => x.Id == parentId);
|
||||
}
|
||||
|
||||
if (mappedCat == null)
|
||||
{
|
||||
// Fallback to Other
|
||||
mappedCat = NewznabStandardCategory.Other;
|
||||
}
|
||||
|
||||
foreach (var xmlSubcat in xmlCategory.Elements("subcat"))
|
||||
{
|
||||
var subCat = new IndexerCategory
|
||||
{
|
||||
Id = int.Parse(xmlSubcat.Attribute("id").Value),
|
||||
Name = xmlSubcat.Attribute("name").Value,
|
||||
Description = xmlSubcat.Attribute("description") != null ? xmlSubcat.Attribute("description").Value : string.Empty
|
||||
};
|
||||
var subName = xmlSubcat.Attribute("name").Value;
|
||||
var subId = int.Parse(xmlSubcat.Attribute("id").Value);
|
||||
|
||||
cat.SubCategories.Add(subCat);
|
||||
capabilities.Categories.AddCategoryMapping(subCat.Name, subCat);
|
||||
var mappingName = $"{mappedCat.Name}/{subName}";
|
||||
var mappedSubCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Name.ToLower() == mappingName.ToLower());
|
||||
|
||||
if (mappedSubCat == null)
|
||||
{
|
||||
// Try by child id if name fails
|
||||
mappedSubCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Id == subId);
|
||||
}
|
||||
|
||||
if (mappedSubCat == null && mappedCat.Id != NewznabStandardCategory.Other.Id)
|
||||
{
|
||||
// Try by Parent/Other if parent is not other
|
||||
mappedSubCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Name.ToLower() == $"{mappedCat.Name.ToLower()}/other");
|
||||
}
|
||||
|
||||
if (mappedSubCat == null)
|
||||
{
|
||||
// Fallback to Misc Other
|
||||
mappedSubCat = NewznabStandardCategory.OtherMisc;
|
||||
}
|
||||
|
||||
if (mappedSubCat != null)
|
||||
{
|
||||
capabilities.Categories.AddCategoryMapping(subId, mappedSubCat, $"{parentName}/{subName}");
|
||||
}
|
||||
}
|
||||
|
||||
capabilities.Categories.AddCategoryMapping(cat.Name, cat);
|
||||
if (mappedCat != null)
|
||||
{
|
||||
capabilities.Categories.AddCategoryMapping(parentId, mappedCat, parentName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
||||
capabilities,
|
||||
parameters));
|
||||
|
||||
return pageableRequests;
|
||||
@@ -109,6 +110,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
||||
capabilities,
|
||||
parameters));
|
||||
|
||||
return pageableRequests;
|
||||
@@ -175,6 +177,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
||||
capabilities,
|
||||
parameters));
|
||||
|
||||
return pageableRequests;
|
||||
@@ -216,6 +219,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
||||
capabilities,
|
||||
parameters));
|
||||
|
||||
return pageableRequests;
|
||||
@@ -233,15 +237,15 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, IndexerCapabilities capabilities, NameValueCollection parameters)
|
||||
{
|
||||
var baseUrl = string.Format("{0}{1}?t={2}&extended=1", Settings.BaseUrl.TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchCriteria.SearchType);
|
||||
var categories = searchCriteria.Categories;
|
||||
var categories = capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
|
||||
if (categories != null && categories.Any())
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Xml.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
@@ -13,12 +14,16 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
public const string ns = "{http://www.newznab.com/DTD/2010/feeds/attributes/}";
|
||||
|
||||
private readonly NewznabSettings _settings;
|
||||
private readonly ProviderDefinition _definition;
|
||||
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
|
||||
|
||||
public NewznabRssParser(NewznabSettings settings)
|
||||
public NewznabRssParser(NewznabSettings settings, ProviderDefinition definition, INewznabCapabilitiesProvider capabilitiesProvider)
|
||||
{
|
||||
PreferredEnclosureMimeTypes = UsenetEnclosureMimeTypes;
|
||||
UseEnclosureUrl = true;
|
||||
_settings = settings;
|
||||
_definition = definition;
|
||||
_capabilitiesProvider = capabilitiesProvider;
|
||||
}
|
||||
|
||||
public static void CheckError(XDocument xdoc, IndexerResponse indexerResponse)
|
||||
@@ -118,19 +123,17 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
|
||||
protected override ICollection<IndexerCategory> GetCategory(XElement item)
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(_settings, _definition);
|
||||
var cats = TryGetMultipleNewznabAttributes(item, "category");
|
||||
var results = new List<IndexerCategory>();
|
||||
|
||||
foreach (var cat in cats)
|
||||
{
|
||||
if (int.TryParse(cat, out var intCategory))
|
||||
{
|
||||
var indexerCat = _settings.Categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
|
||||
var indexerCat = capabilities.Categories.MapTrackerCatToNewznab(cat);
|
||||
|
||||
if (indexerCat != null)
|
||||
{
|
||||
results.Add(indexerCat);
|
||||
}
|
||||
if (indexerCat != null)
|
||||
{
|
||||
results.AddRange(indexerCat);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
|
||||
public NewznabSettingsValidator()
|
||||
{
|
||||
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
|
||||
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new TorznabRssParser(Settings);
|
||||
return new TorznabRssParser(Settings, Definition, _capabilitiesProvider);
|
||||
}
|
||||
|
||||
public string[] GetBaseUrlFromSettings()
|
||||
@@ -157,13 +157,13 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
}
|
||||
|
||||
if (capabilities.MovieSearchParams != null &&
|
||||
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
|
||||
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId, MovieSearchParam.TraktId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (capabilities.TvSearchParams != null &&
|
||||
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
|
||||
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.ImdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
|
||||
new[] { TvSearchParam.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -4,7 +4,9 @@ using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Torznab
|
||||
{
|
||||
@@ -13,10 +15,15 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
public const string ns = "{http://torznab.com/schemas/2015/feed}";
|
||||
|
||||
private readonly TorznabSettings _settings;
|
||||
public TorznabRssParser(TorznabSettings settings)
|
||||
private readonly ProviderDefinition _definition;
|
||||
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
|
||||
|
||||
public TorznabRssParser(TorznabSettings settings, ProviderDefinition definition, INewznabCapabilitiesProvider capabilitiesProvider)
|
||||
{
|
||||
UseEnclosureUrl = true;
|
||||
_settings = settings;
|
||||
_definition = definition;
|
||||
_capabilitiesProvider = capabilitiesProvider;
|
||||
}
|
||||
|
||||
protected override bool PreProcess(IndexerResponse indexerResponse)
|
||||
@@ -157,19 +164,17 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
|
||||
protected override ICollection<IndexerCategory> GetCategory(XElement item)
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(_settings, _definition);
|
||||
var cats = TryGetMultipleNewznabAttributes(item, "category");
|
||||
var results = new List<IndexerCategory>();
|
||||
|
||||
foreach (var cat in cats)
|
||||
{
|
||||
if (int.TryParse(cat, out var intCategory))
|
||||
{
|
||||
var indexerCat = _settings.Categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
|
||||
var indexerCat = capabilities.Categories.MapTrackerCatToNewznab(cat);
|
||||
|
||||
if (indexerCat != null)
|
||||
{
|
||||
results.Add(indexerCat);
|
||||
}
|
||||
if (indexerCat != null)
|
||||
{
|
||||
results.AddRange(indexerCat);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
|
||||
public TorznabSettingsValidator()
|
||||
{
|
||||
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
|
||||
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
|
||||
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user