mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
77 Commits
v1.14.0.42
...
v1.16.0.44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37a6d03d52 | ||
|
|
fe35d450f0 | ||
|
|
6a9e27bc06 | ||
|
|
a989bf82ea | ||
|
|
ccc8d8002f | ||
|
|
eaaf8db486 | ||
|
|
c32fa7a84b | ||
|
|
57e21a78ee | ||
|
|
9cdf5d18d8 | ||
|
|
41b0a1211b | ||
|
|
1b8f09f2ce | ||
|
|
2f85de6b69 | ||
|
|
b2ef9d5b0a | ||
|
|
c80262d75b | ||
|
|
2a312d93ec | ||
|
|
e09df2fff3 | ||
|
|
f0c7d13b20 | ||
|
|
4dac60bef9 | ||
|
|
5aefb46790 | ||
|
|
41b043e551 | ||
|
|
5447fad1fc | ||
|
|
6a1e01abbd | ||
|
|
2803ad5ba0 | ||
|
|
8fa8a13036 | ||
|
|
41ce79ccce | ||
|
|
14ae062db2 | ||
|
|
d55a38da4a | ||
|
|
ab289cfc86 | ||
|
|
12671e9905 | ||
|
|
a33023b8c6 | ||
|
|
a3e134ce0b | ||
|
|
ee7c821cab | ||
|
|
ee4cf93aee | ||
|
|
2cacfba81f | ||
|
|
02e420580e | ||
|
|
d99398d3f8 | ||
|
|
9ea8335aa0 | ||
|
|
52a91a50b2 | ||
|
|
680bf46e25 | ||
|
|
d279c97f15 | ||
|
|
7d5d338c8e | ||
|
|
721ae1cac0 | ||
|
|
3881c9d753 | ||
|
|
131b344119 | ||
|
|
d226e52881 | ||
|
|
583815b4f7 | ||
|
|
f0a8d22e84 | ||
|
|
50c6f15e12 | ||
|
|
2e3a95f389 | ||
|
|
3d52096eb4 | ||
|
|
e981cacbda | ||
|
|
218371a318 | ||
|
|
30fd7c8c2a | ||
|
|
96d2d61fa0 | ||
|
|
17ff86aaea | ||
|
|
7f8c1ace14 | ||
|
|
dc0edb7bc1 | ||
|
|
2ac996c9f9 | ||
|
|
2ebabd69b5 | ||
|
|
b3738f1602 | ||
|
|
882152b911 | ||
|
|
a25e79031f | ||
|
|
cc85060b1b | ||
|
|
00bd9c241a | ||
|
|
1283e06f95 | ||
|
|
ab0108778a | ||
|
|
099b04f718 | ||
|
|
ecdc0a51a9 | ||
|
|
6c7c37affe | ||
|
|
45d378a2d9 | ||
|
|
007601cb19 | ||
|
|
5f0d6e2fdd | ||
|
|
ede9879c99 | ||
|
|
7287abc77c | ||
|
|
8c653b5c09 | ||
|
|
15c6b3c308 | ||
|
|
9676447c74 |
@@ -9,14 +9,14 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.14.0'
|
||||
majorVersion: '1.16.0'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.417'
|
||||
nodeVersion: '16.X'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface CommandBody {
|
||||
lastStartTime: string;
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
seriesId?: number;
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { maxBy } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
|
||||
if (id) {
|
||||
dispatchSetFilter({ selectedFilterKey: id });
|
||||
} else {
|
||||
const last = customFilters[customFilters.length -1];
|
||||
const last = maxBy(customFilters, 'id');
|
||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||
}
|
||||
|
||||
@@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
labelErrors: [
|
||||
{
|
||||
message: 'Label is required'
|
||||
message: translate('LabelIsRequired')
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Custom Filter
|
||||
{translate('CustomFilter')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
Label
|
||||
{translate('Label')}
|
||||
</div>
|
||||
|
||||
<div className={styles.labelInputContainer}>
|
||||
|
||||
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
||||
dispatchSetFilter
|
||||
} = this.props;
|
||||
|
||||
// Assume that delete and then unmounting means the delete was successful.
|
||||
// Moving this check to a ancestor would be more accurate, but would have
|
||||
// Assume that delete and then unmounting means the deletion was successful.
|
||||
// Moving this check to an ancestor would be more accurate, but would have
|
||||
// more boilerplate.
|
||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||
|
||||
@@ -24,16 +24,20 @@ function createMapStateToProps() {
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: true
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
get value() {
|
||||
return `(${translate('Mixed')})`;
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
const availabilityOptions = [
|
||||
{ key: 'announced', value: 'Announced' },
|
||||
{ key: 'inCinemas', value: 'In Cinemas' },
|
||||
{ key: 'released', value: 'Released' },
|
||||
{ key: 'preDB', value: 'PreDB' }
|
||||
];
|
||||
|
||||
function AvailabilitySelectInput(props) {
|
||||
const values = [...availabilityOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed
|
||||
} = props;
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
{...props}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
AvailabilitySelectInput.propTypes = {
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
includeMixed: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AvailabilitySelectInput.defaultProps = {
|
||||
includeNoChange: false,
|
||||
includeMixed: false
|
||||
};
|
||||
|
||||
export default AvailabilitySelectInput;
|
||||
@@ -5,7 +5,6 @@ import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AppProfileSelectInputConnector from './AppProfileSelectInputConnector';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
@@ -37,9 +36,6 @@ function getComponent(type) {
|
||||
case inputTypes.AUTO_COMPLETE:
|
||||
return AutoCompleteInput;
|
||||
|
||||
case inputTypes.AVAILABILITY_SELECT:
|
||||
return AvailabilitySelectInput;
|
||||
|
||||
case inputTypes.CAPTCHA:
|
||||
return CaptchaInputConnector;
|
||||
|
||||
|
||||
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal file
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export default function useModalOpenState(
|
||||
initialState: boolean
|
||||
): [boolean, () => void, () => void] {
|
||||
const [isOpen, setOpen] = useState(initialState);
|
||||
|
||||
const setModalOpen = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, [setOpen]);
|
||||
|
||||
const setModalClosed = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
return [isOpen, setModalOpen, setModalClosed];
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
export const AUTO_COMPLETE = 'autoComplete';
|
||||
export const APP_PROFILE_SELECT = 'appProfileSelect';
|
||||
export const AVAILABILITY_SELECT = 'availabilitySelect';
|
||||
export const CAPTCHA = 'captcha';
|
||||
export const CARDIGANNCAPTCHA = 'cardigannCaptcha';
|
||||
export const CHECK = 'check';
|
||||
@@ -27,7 +26,6 @@ export const TAG_SELECT = 'tagSelect';
|
||||
export const all = [
|
||||
AUTO_COMPLETE,
|
||||
APP_PROFILE_SELECT,
|
||||
AVAILABILITY_SELECT,
|
||||
CAPTCHA,
|
||||
CARDIGANNCAPTCHA,
|
||||
CHECK,
|
||||
|
||||
@@ -144,6 +144,7 @@ function EditIndexerModalContent(props) {
|
||||
}) :
|
||||
null
|
||||
}
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -35,7 +35,7 @@ const enableOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'true',
|
||||
|
||||
@@ -47,9 +47,3 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraSmall) {
|
||||
.modalFooter {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,11 @@ class SearchFooter extends Component {
|
||||
name="searchQuery"
|
||||
value={searchQuery}
|
||||
buttons={
|
||||
<FormInputButton onPress={this.onQueryParameterModalOpenClick}>
|
||||
<FormInputButton
|
||||
kind={kinds.DEFAULT}
|
||||
onPress={this.onQueryParameterModalOpenClick}
|
||||
title={translate('ClickToChangeQueryOptions')}
|
||||
>
|
||||
<Icon
|
||||
name={icon}
|
||||
/>
|
||||
@@ -275,6 +279,7 @@ class SearchFooter extends Component {
|
||||
}
|
||||
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
className={styles.searchButton}
|
||||
isSpinning={isFetching}
|
||||
isDisabled={isFetching || !hasIndexers}
|
||||
|
||||
@@ -30,7 +30,7 @@ const syncLevelOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: ApplicationSyncLevel.Disabled,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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 Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddDownloadClientModal from './AddDownloadClientModal';
|
||||
import DownloadClient from './DownloadClient';
|
||||
@@ -59,48 +60,59 @@ class DownloadClients extends Component {
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('DownloadClients')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('DownloadClientsLoadError')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.downloadClients}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<DownloadClient
|
||||
key={item.id}
|
||||
{...item}
|
||||
onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addDownloadClient}
|
||||
onPress={this.onAddDownloadClientPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('ProwlarrDownloadClientsAlert')}
|
||||
</div>
|
||||
<div>
|
||||
{translate('ProwlarrDownloadClientsInAppOnlyAlert')}
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<AddDownloadClientModal
|
||||
isOpen={isAddDownloadClientModalOpen}
|
||||
onModalClose={this.onAddDownloadClientModalClose}
|
||||
/>
|
||||
<FieldSet legend={translate('DownloadClients')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('DownloadClientsLoadError')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.downloadClients}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<DownloadClient
|
||||
key={item.id}
|
||||
{...item}
|
||||
onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<EditDownloadClientModalConnector
|
||||
isOpen={isEditDownloadClientModalOpen}
|
||||
onModalClose={this.onEditDownloadClientModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
<Card
|
||||
className={styles.addDownloadClient}
|
||||
onPress={this.onAddDownloadClientPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AddDownloadClientModal
|
||||
isOpen={isAddDownloadClientModalOpen}
|
||||
onModalClose={this.onAddDownloadClientModalClose}
|
||||
/>
|
||||
|
||||
<EditDownloadClientModalConnector
|
||||
isOpen={isEditDownloadClientModalOpen}
|
||||
onModalClose={this.onEditDownloadClientModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddCategoryModalConnector from './Categories/AddCategoryModalConnector';
|
||||
import Category from './Categories/Category';
|
||||
@@ -61,6 +62,7 @@ class EditDownloadClientModalContent extends Component {
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteDownloadClientPress,
|
||||
onConfirmDeleteCategory,
|
||||
...otherProps
|
||||
@@ -219,6 +221,12 @@ class EditDownloadClientModalContent extends Component {
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
@@ -260,6 +268,7 @@ EditDownloadClientModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteDownloadClientPress: PropTypes.func,
|
||||
onConfirmDeleteCategory: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -2,7 +2,15 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteDownloadClientCategory, fetchDownloadClientCategories, saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
deleteDownloadClientCategory,
|
||||
fetchDownloadClientCategories,
|
||||
saveDownloadClient,
|
||||
setDownloadClientFieldValue,
|
||||
setDownloadClientValue,
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||
|
||||
@@ -27,7 +35,8 @@ const mapDispatchToProps = {
|
||||
saveDownloadClient,
|
||||
testDownloadClient,
|
||||
fetchDownloadClientCategories,
|
||||
deleteDownloadClientCategory
|
||||
deleteDownloadClientCategory,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditDownloadClientModalContentConnector extends Component {
|
||||
@@ -68,6 +77,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
this.props.testDownloadClient({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
onConfirmDeleteCategory = (id) => {
|
||||
this.props.deleteDownloadClientCategory({ id });
|
||||
};
|
||||
@@ -81,6 +94,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
onConfirmDeleteCategory={this.onConfirmDeleteCategory}
|
||||
@@ -102,6 +116,7 @@ EditDownloadClientModalContentConnector.propTypes = {
|
||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||
saveDownloadClient: PropTypes.func.isRequired,
|
||||
testDownloadClient: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const enableOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditIndexerProxyModalContent.css';
|
||||
|
||||
@@ -31,6 +32,7 @@ function EditIndexerProxyModalContent(props) {
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteIndexerProxyPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
@@ -130,6 +132,12 @@ function EditIndexerProxyModalContent(props) {
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
@@ -169,6 +177,7 @@ EditIndexerProxyModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteIndexerProxyPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveIndexerProxy, setIndexerProxyFieldValue, setIndexerProxyValue, testIndexerProxy } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveIndexerProxy,
|
||||
setIndexerProxyFieldValue,
|
||||
setIndexerProxyValue,
|
||||
testIndexerProxy,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditIndexerProxyModalContent from './EditIndexerProxyModalContent';
|
||||
|
||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
||||
setIndexerProxyValue,
|
||||
setIndexerProxyFieldValue,
|
||||
saveIndexerProxy,
|
||||
testIndexerProxy
|
||||
testIndexerProxy,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditIndexerProxyModalContentConnector extends Component {
|
||||
@@ -56,6 +63,10 @@ class EditIndexerProxyModalContentConnector extends Component {
|
||||
this.props.testIndexerProxy({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -65,6 +76,7 @@ class EditIndexerProxyModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
@@ -82,6 +94,7 @@ EditIndexerProxyModalContentConnector.propTypes = {
|
||||
setIndexerProxyFieldValue: PropTypes.func.isRequired,
|
||||
saveIndexerProxy: PropTypes.func.isRequired,
|
||||
testIndexerProxy: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NotificationEventItems from './NotificationEventItems';
|
||||
import styles from './EditNotificationModalContent.css';
|
||||
@@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteNotificationPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
@@ -136,6 +138,12 @@ function EditNotificationModalContent(props) {
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
@@ -175,6 +183,7 @@ EditNotificationModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteNotificationPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveNotification,
|
||||
setNotificationFieldValue,
|
||||
setNotificationValue,
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditNotificationModalContent from './EditNotificationModalContent';
|
||||
|
||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
||||
setNotificationValue,
|
||||
setNotificationFieldValue,
|
||||
saveNotification,
|
||||
testNotification
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditNotificationModalContentConnector extends Component {
|
||||
@@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
|
||||
this.props.testNotification({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
@@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
|
||||
setNotificationFieldValue: PropTypes.func.isRequired,
|
||||
saveNotification: PropTypes.func.isRequired,
|
||||
testNotification: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -41,6 +41,14 @@ function getInternalLink(source) {
|
||||
to="/settings/connect"
|
||||
/>
|
||||
);
|
||||
case 'IndexerProxyStatusCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
to="/settings/indexers"
|
||||
/>
|
||||
);
|
||||
case 'IndexerRssCheck':
|
||||
case 'IndexerSearchCheck':
|
||||
case 'IndexerStatusCheck':
|
||||
|
||||
@@ -10,15 +10,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.commandName {
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.queued,
|
||||
.started,
|
||||
.ended {
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'commandName': string;
|
||||
'duration': string;
|
||||
'ended': string;
|
||||
'queued': string;
|
||||
'started': string;
|
||||
'trigger': string;
|
||||
'triggerContent': string;
|
||||
'userAgent': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueuedTaskRow.css';
|
||||
|
||||
function getStatusIconProps(status, message) {
|
||||
const title = titleCase(status);
|
||||
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return {
|
||||
name: icons.PENDING,
|
||||
title
|
||||
};
|
||||
|
||||
case 'started':
|
||||
return {
|
||||
name: icons.REFRESH,
|
||||
isSpinning: true,
|
||||
title
|
||||
};
|
||||
|
||||
case 'completed':
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS,
|
||||
title: message === 'Completed' ? title : `${title}: ${message}`
|
||||
};
|
||||
|
||||
case 'failed':
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kind: kinds.DANGER,
|
||||
title: `${title}: ${message}`
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
name: icons.UNKNOWN,
|
||||
title
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFormattedDates(props) {
|
||||
const {
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
} = props;
|
||||
|
||||
if (showRelativeDates) {
|
||||
return {
|
||||
queuedAt: moment(queued).fromNow(),
|
||||
startedAt: started ? moment(started).fromNow() : '-',
|
||||
endedAt: ended ? moment(ended).fromNow() : '-'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queuedAt: formatDate(queued, shortDateFormat),
|
||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
|
||||
};
|
||||
}
|
||||
|
||||
class QueuedTaskRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
...getFormattedDates(props),
|
||||
isCancelConfirmModalOpen: false
|
||||
};
|
||||
|
||||
this._updateTimeoutId = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setUpdateTimer();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
queued,
|
||||
started,
|
||||
ended
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
queued !== prevProps.queued ||
|
||||
started !== prevProps.started ||
|
||||
ended !== prevProps.ended
|
||||
) {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._updateTimeoutId) {
|
||||
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setUpdateTimer() {
|
||||
this._updateTimeoutId = setTimeout(() => {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
this.setUpdateTimer();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCancelPress = () => {
|
||||
this.setState({
|
||||
isCancelConfirmModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onAbortCancel = () => {
|
||||
this.setState({
|
||||
isCancelConfirmModalOpen: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
trigger,
|
||||
commandName,
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
status,
|
||||
duration,
|
||||
message,
|
||||
clientUserAgent,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onCancelPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
queuedAt,
|
||||
startedAt,
|
||||
endedAt,
|
||||
isCancelConfirmModalOpen
|
||||
} = this.state;
|
||||
|
||||
let triggerIcon = icons.QUICK;
|
||||
|
||||
if (trigger === 'manual') {
|
||||
triggerIcon = icons.INTERACTIVE;
|
||||
} else if (trigger === 'scheduled') {
|
||||
triggerIcon = icons.SCHEDULED;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.trigger}>
|
||||
<span className={styles.triggerContent}>
|
||||
<Icon
|
||||
name={triggerIcon}
|
||||
title={titleCase(trigger)}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
{...getStatusIconProps(status, message)}
|
||||
/>
|
||||
</span>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<span className={styles.commandName}>
|
||||
{commandName}
|
||||
</span>
|
||||
{
|
||||
clientUserAgent ?
|
||||
<span className={styles.userAgent} title={translate('UserAgentProvidedByTheAppThatCalledTheAPI')}>
|
||||
from: {clientUserAgent}
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.queued}
|
||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||
>
|
||||
{queuedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.started}
|
||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||
>
|
||||
{startedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.ended}
|
||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||
>
|
||||
{endedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.duration}>
|
||||
{formatTimeSpan(duration)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.actions}
|
||||
>
|
||||
{
|
||||
status === 'queued' &&
|
||||
<IconButton
|
||||
title={translate('RemovedFromTaskQueue')}
|
||||
name={icons.REMOVE}
|
||||
onPress={this.onCancelPress}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isCancelConfirmModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('Cancel')}
|
||||
message={translate('CancelPendingTask')}
|
||||
confirmLabel={translate('YesCancel')}
|
||||
cancelLabel={translate('NoLeaveIt')}
|
||||
onConfirm={onCancelPress}
|
||||
onCancel={this.onAbortCancel}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueuedTaskRow.propTypes = {
|
||||
trigger: PropTypes.string.isRequired,
|
||||
commandName: PropTypes.string.isRequired,
|
||||
queued: PropTypes.string.isRequired,
|
||||
started: PropTypes.string,
|
||||
ended: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
duration: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
clientUserAgent: PropTypes.string,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onCancelPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QueuedTaskRow;
|
||||
238
frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
Normal file
238
frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { CommandBody } from 'Commands/Command';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
|
||||
import styles from './QueuedTaskRow.css';
|
||||
|
||||
function getStatusIconProps(status: string, message: string | undefined) {
|
||||
const title = titleCase(status);
|
||||
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return {
|
||||
name: icons.PENDING,
|
||||
title,
|
||||
};
|
||||
|
||||
case 'started':
|
||||
return {
|
||||
name: icons.REFRESH,
|
||||
isSpinning: true,
|
||||
title,
|
||||
};
|
||||
|
||||
case 'completed':
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS,
|
||||
title: message === 'Completed' ? title : `${title}: ${message}`,
|
||||
};
|
||||
|
||||
case 'failed':
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kind: kinds.DANGER,
|
||||
title: `${title}: ${message}`,
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
name: icons.UNKNOWN,
|
||||
title,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFormattedDates(
|
||||
queued: string,
|
||||
started: string | undefined,
|
||||
ended: string | undefined,
|
||||
showRelativeDates: boolean,
|
||||
shortDateFormat: string
|
||||
) {
|
||||
if (showRelativeDates) {
|
||||
return {
|
||||
queuedAt: moment(queued).fromNow(),
|
||||
startedAt: started ? moment(started).fromNow() : '-',
|
||||
endedAt: ended ? moment(ended).fromNow() : '-',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queuedAt: formatDate(queued, shortDateFormat),
|
||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
|
||||
};
|
||||
}
|
||||
|
||||
interface QueuedTimes {
|
||||
queuedAt: string;
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
}
|
||||
|
||||
export interface QueuedTaskRowProps {
|
||||
id: number;
|
||||
trigger: string;
|
||||
commandName: string;
|
||||
queued: string;
|
||||
started?: string;
|
||||
ended?: string;
|
||||
status: string;
|
||||
duration?: string;
|
||||
message?: string;
|
||||
body: CommandBody;
|
||||
clientUserAgent?: string;
|
||||
}
|
||||
|
||||
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
|
||||
const {
|
||||
id,
|
||||
trigger,
|
||||
commandName,
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
status,
|
||||
duration,
|
||||
message,
|
||||
body,
|
||||
clientUserAgent,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
);
|
||||
const [times, setTimes] = useState<QueuedTimes>(
|
||||
getFormattedDates(
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
)
|
||||
);
|
||||
|
||||
const [
|
||||
isCancelConfirmModalOpen,
|
||||
openCancelConfirmModal,
|
||||
closeCancelConfirmModal,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const handleCancelPress = useCallback(() => {
|
||||
dispatch(cancelCommand({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
updateTimeTimeoutId.current = setTimeout(() => {
|
||||
setTimes(
|
||||
getFormattedDates(
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
)
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
return () => {
|
||||
if (updateTimeTimeoutId.current) {
|
||||
clearTimeout(updateTimeTimeoutId.current);
|
||||
}
|
||||
};
|
||||
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
|
||||
|
||||
const { queuedAt, startedAt, endedAt } = times;
|
||||
|
||||
let triggerIcon = icons.QUICK;
|
||||
|
||||
if (trigger === 'manual') {
|
||||
triggerIcon = icons.INTERACTIVE;
|
||||
} else if (trigger === 'scheduled') {
|
||||
triggerIcon = icons.SCHEDULED;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.trigger}>
|
||||
<span className={styles.triggerContent}>
|
||||
<Icon name={triggerIcon} title={titleCase(trigger)} />
|
||||
|
||||
<Icon {...getStatusIconProps(status, message)} />
|
||||
</span>
|
||||
</TableRowCell>
|
||||
|
||||
<QueuedTaskRowNameCell
|
||||
commandName={commandName}
|
||||
body={body}
|
||||
clientUserAgent={clientUserAgent}
|
||||
/>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.queued}
|
||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||
>
|
||||
{queuedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.started}
|
||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||
>
|
||||
{startedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.ended}
|
||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||
>
|
||||
{endedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.duration}>
|
||||
{formatTimeSpan(duration)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
{status === 'queued' && (
|
||||
<IconButton
|
||||
title={translate('RemovedFromTaskQueue')}
|
||||
name={icons.REMOVE}
|
||||
onPress={openCancelConfirmModal}
|
||||
/>
|
||||
)}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isCancelConfirmModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('Cancel')}
|
||||
message={translate('CancelPendingTask')}
|
||||
confirmLabel={translate('YesCancel')}
|
||||
cancelLabel={translate('NoLeaveIt')}
|
||||
onConfirm={handleCancelPress}
|
||||
onCancel={closeCancelConfirmModal}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import QueuedTaskRow from './QueuedTaskRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onCancelPress() {
|
||||
dispatch(cancelCommand({
|
||||
id: props.id
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
|
||||
@@ -0,0 +1,8 @@
|
||||
.commandName {
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
8
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
vendored
Normal file
8
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'commandName': string;
|
||||
'userAgent': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
32
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
Normal file
32
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { CommandBody } from 'Commands/Command';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueuedTaskRowNameCell.css';
|
||||
|
||||
export interface QueuedTaskRowNameCellProps {
|
||||
commandName: string;
|
||||
body: CommandBody;
|
||||
clientUserAgent?: string;
|
||||
}
|
||||
|
||||
export default function QueuedTaskRowNameCell(
|
||||
props: QueuedTaskRowNameCellProps
|
||||
) {
|
||||
const { commandName, clientUserAgent } = props;
|
||||
|
||||
return (
|
||||
<TableRowCell>
|
||||
<span className={styles.commandName}>{commandName}</span>
|
||||
|
||||
{clientUserAgent ? (
|
||||
<span
|
||||
className={styles.userAgent}
|
||||
title={translate('TaskUserAgentTooltip')}
|
||||
>
|
||||
{translate('From')}: {clientUserAgent}
|
||||
</span>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'trigger',
|
||||
label: '',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'commandName',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'queued',
|
||||
label: () => translate('Queued'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'started',
|
||||
label: () => translate('Started'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'ended',
|
||||
label: () => translate('Ended'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: () => translate('Duration'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function QueuedTasks(props) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Queue')}>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<QueuedTaskRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
QueuedTasks.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default QueuedTasks;
|
||||
74
frontend/src/System/Tasks/Queued/QueuedTasks.tsx
Normal file
74
frontend/src/System/Tasks/Queued/QueuedTasks.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRow from './QueuedTaskRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'trigger',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'commandName',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'queued',
|
||||
label: () => translate('Queued'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'started',
|
||||
label: () => translate('Started'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'ended',
|
||||
label: () => translate('Ended'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: () => translate('Duration'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function QueuedTasks() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isPopulated, items } = useSelector(
|
||||
(state: AppState) => state.commands
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCommands());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Queue')}>
|
||||
{isFetching && !isPopulated && <LoadingIndicator />}
|
||||
|
||||
{isPopulated && (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return <QueuedTaskRow key={item.id} {...item} />;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||
import QueuedTasks from './QueuedTasks';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.commands,
|
||||
(commands) => {
|
||||
return commands;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchCommands: fetchCommands
|
||||
};
|
||||
|
||||
class QueuedTasksConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchCommands();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QueuedTasks
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueuedTasksConnector.propTypes = {
|
||||
dispatchFetchCommands: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
|
||||
import QueuedTasks from './Queued/QueuedTasks';
|
||||
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||
|
||||
function Tasks() {
|
||||
@@ -10,7 +10,7 @@ function Tasks() {
|
||||
<PageContent title={translate('Tasks')}>
|
||||
<PageContentBody>
|
||||
<ScheduledTasksConnector />
|
||||
<QueuedTasksConnector />
|
||||
<QueuedTasks />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#e66001" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
<meta name="msapplication-navbutton-color" content="#464b51" />
|
||||
<!-- Android/Apple Phone -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<meta name="description" content="Prowlarr" />
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#e66001" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#464b51" />
|
||||
<!-- Android/Apple Phone -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<meta name="description" content="Prowlarr (Preview)" />
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
||||
"lint-fix": "yarn lint --fix",
|
||||
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
|
||||
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc",
|
||||
"stylelint-windows": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc",
|
||||
"check-modules": "are-you-es5 check . -r"
|
||||
},
|
||||
"repository": "https://github.com/Prowlarr/Prowlarr",
|
||||
|
||||
@@ -119,6 +119,10 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
||||
|
||||
// Discord
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
|
||||
public void should_clean_message(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
@@ -29,11 +32,14 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
||||
private readonly ICached<CredentialCache> _credentialCache;
|
||||
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
||||
ICreateManagedWebProxy createManagedWebProxy,
|
||||
ICertificateValidationService certificateValidationService,
|
||||
IUserAgentBuilder userAgentBuilder,
|
||||
ICacheManager cacheManager)
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_proxySettingsProvider = proxySettingsProvider;
|
||||
_createManagedWebProxy = createManagedWebProxy;
|
||||
@@ -42,6 +48,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
||||
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
|
||||
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
||||
@@ -275,7 +283,27 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
private bool HasRoutableIPv4Address()
|
||||
{
|
||||
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
|
||||
try
|
||||
{
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
|
||||
return networkInterfaces.Any(ni =>
|
||||
ni.OperationalStatus == OperationalStatus.Up &&
|
||||
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip.Address)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
||||
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
||||
@@ -298,10 +326,10 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
}
|
||||
catch
|
||||
{
|
||||
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
|
||||
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
|
||||
// but in the interest of keeping this implementation simple, this is acceptable.
|
||||
useIPv6 = false;
|
||||
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
|
||||
var routableIPv4 = HasRoutableIPv4Address();
|
||||
_logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled");
|
||||
useIPv6 = !routableIPv4;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -61,6 +61,9 @@ namespace NzbDrone.Common.Instrumentation
|
||||
|
||||
// Applications
|
||||
new (@"""name"":""apikey"",""value"":""(?<secret>[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Discord
|
||||
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class newznab_category_to_capabilities_settingsFixture : MigrationTest<newznab_category_to_capabilities_settings>
|
||||
{
|
||||
[Test]
|
||||
public void should_migrate_categories_when_capabilities_is_not_defined()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Indexers").Row(new
|
||||
{
|
||||
Name = "Usenet Indexer",
|
||||
Redirect = false,
|
||||
AppProfileId = 0,
|
||||
DownloadClientId = 0,
|
||||
Priority = 25,
|
||||
Added = DateTime.UtcNow,
|
||||
Implementation = "Newznab",
|
||||
Settings = new
|
||||
{
|
||||
Categories = new[]
|
||||
{
|
||||
new { Id = 2000, Name = "Movies" },
|
||||
new { Id = 5000, Name = "TV" }
|
||||
}
|
||||
}.ToJson(),
|
||||
ConfigContract = "NewznabSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<IndexerDefinition40>("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Newznab");
|
||||
items.First().ConfigContract.Should().Be("NewznabSettings");
|
||||
items.First().Settings.Should().ContainKey("capabilities");
|
||||
items.First().Settings.Should().NotContainKey("categories");
|
||||
|
||||
var newznabSettings = items.First().Settings.ToObject<NewznabSettings40>();
|
||||
newznabSettings.Capabilities.Should().NotBeNull();
|
||||
newznabSettings.Capabilities.SupportsRawSearch.Should().Be(false);
|
||||
newznabSettings.Capabilities.Categories.Should().HaveCount(2);
|
||||
newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 2000 && c.Name == "Movies");
|
||||
newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 5000 && c.Name == "TV");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_migrate_categories_when_capabilities_is_defined()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Indexers").Row(new
|
||||
{
|
||||
Name = "Usenet Indexer",
|
||||
Redirect = false,
|
||||
AppProfileId = 0,
|
||||
DownloadClientId = 0,
|
||||
Priority = 25,
|
||||
Added = DateTime.UtcNow,
|
||||
Implementation = "Newznab",
|
||||
Settings = new
|
||||
{
|
||||
Capabilities = new
|
||||
{
|
||||
SupportsRawSearch = true
|
||||
},
|
||||
Categories = new[]
|
||||
{
|
||||
new { Id = 2000, Name = "Movies" },
|
||||
new { Id = 5000, Name = "TV" }
|
||||
}
|
||||
}.ToJson(),
|
||||
ConfigContract = "NewznabSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<IndexerDefinition40>("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Newznab");
|
||||
items.First().ConfigContract.Should().Be("NewznabSettings");
|
||||
items.First().Settings.Should().ContainKey("capabilities");
|
||||
items.First().Settings.Should().NotContainKey("categories");
|
||||
|
||||
var newznabSettings = items.First().Settings.ToObject<NewznabSettings40>();
|
||||
newznabSettings.Capabilities.Should().NotBeNull();
|
||||
newznabSettings.Capabilities.SupportsRawSearch.Should().Be(true);
|
||||
newznabSettings.Capabilities.Categories.Should().HaveCount(2);
|
||||
newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 2000 && c.Name == "Movies");
|
||||
newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 5000 && c.Name == "TV");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_defaults_when_categories_are_empty()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Indexers").Row(new
|
||||
{
|
||||
Name = "Usenet Indexer",
|
||||
Redirect = false,
|
||||
AppProfileId = 0,
|
||||
DownloadClientId = 0,
|
||||
Priority = 25,
|
||||
Added = DateTime.UtcNow,
|
||||
Implementation = "Newznab",
|
||||
Settings = new
|
||||
{
|
||||
Categories = Array.Empty<object>()
|
||||
}.ToJson(),
|
||||
ConfigContract = "NewznabSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<IndexerDefinition40>("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Newznab");
|
||||
items.First().ConfigContract.Should().Be("NewznabSettings");
|
||||
items.First().Settings.Should().ContainKey("capabilities");
|
||||
items.First().Settings.Should().NotContainKey("categories");
|
||||
|
||||
var newznabSettings = items.First().Settings.ToObject<NewznabSettings40>();
|
||||
newznabSettings.Capabilities.Should().NotBeNull();
|
||||
newznabSettings.Capabilities.SupportsRawSearch.Should().Be(false);
|
||||
newznabSettings.Capabilities.Categories.Should().NotBeNull();
|
||||
newznabSettings.Capabilities.Categories.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_defaults_when_settings_are_empty()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Indexers").Row(new
|
||||
{
|
||||
Name = "Usenet Indexer",
|
||||
Redirect = false,
|
||||
AppProfileId = 0,
|
||||
DownloadClientId = 0,
|
||||
Priority = 25,
|
||||
Added = DateTime.UtcNow,
|
||||
Implementation = "Newznab",
|
||||
Settings = new { }.ToJson(),
|
||||
ConfigContract = "NewznabSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<IndexerDefinition40>("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Newznab");
|
||||
items.First().ConfigContract.Should().Be("NewznabSettings");
|
||||
items.First().Settings.Should().NotContainKey("capabilities");
|
||||
items.First().Settings.Should().NotContainKey("categories");
|
||||
items.First().Settings.ToObject<NewznabSettings40>().Capabilities.Should().BeNull();
|
||||
}
|
||||
}
|
||||
|
||||
public class IndexerDefinition40
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public JObject Settings { get; set; }
|
||||
}
|
||||
|
||||
public class NewznabSettings39
|
||||
{
|
||||
public object Categories { get; set; }
|
||||
}
|
||||
|
||||
public class NewznabSettings40
|
||||
{
|
||||
public NewznabCapabilitiesSettings40 Capabilities { get; set; }
|
||||
}
|
||||
|
||||
public class NewznabCapabilitiesSettings40
|
||||
{
|
||||
public bool SupportsRawSearch { get; set; }
|
||||
public List<IndexerCategory40> Categories { get; set; }
|
||||
}
|
||||
|
||||
public class IndexerCategory40
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework
|
||||
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
||||
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
||||
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
|
||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
|
||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger));
|
||||
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
|
||||
Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder());
|
||||
}
|
||||
|
||||
@@ -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 22:26:21"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 21: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 10:04:50"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 09: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 05:24:49"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 04:24:49"));
|
||||
torrentInfo.Size.Should().Be(69914591044);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 19:20:19"));
|
||||
torrentInfo.Size.Should().Be(8300512414);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
};
|
||||
|
||||
_caps = new IndexerCapabilities();
|
||||
|
||||
_caps.Categories.AddCategoryMapping(2000, NewznabStandardCategory.Movies, "Movies");
|
||||
_caps.Categories.AddCategoryMapping(5000, NewznabStandardCategory.TV, "TV");
|
||||
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_caps);
|
||||
|
||||
@@ -37,6 +37,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
|
||||
_caps.Categories.AddCategoryMapping(2000, NewznabStandardCategory.Movies, "Movies");
|
||||
_caps.Categories.AddCategoryMapping(2040, NewznabStandardCategory.MoviesHD, "Movies/HD");
|
||||
_caps.Categories.AddCategoryMapping(5000, NewznabStandardCategory.TV, "TV");
|
||||
_caps.Categories.AddCategoryMapping(5040, NewznabStandardCategory.TVHD, "TV/HD");
|
||||
_caps.Categories.AddCategoryMapping(5070, NewznabStandardCategory.TVAnime, "TV/Anime");
|
||||
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
@@ -83,23 +86,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
|
||||
|
||||
releases.Should().HaveCount(5);
|
||||
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
var releaseInfo = releases.First() as TorrentInfo;
|
||||
|
||||
releaseInfo.Title.Should().Be("Series Title S05E02 HDTV x264-Xclusive [eztv]");
|
||||
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
releaseInfo.MagnetUrl.Should().Be("magnet:?xt=urn:btih:9fb267cff5ae5603f07a347676ec3bf3e35f75e1&dn=Game+of+Thrones+S05E02+HDTV+x264-Xclusive+%5Beztv%5D&tr=udp:%2F%2Fopen.demonii.com:1337&tr=udp:%2F%2Ftracker.coppersurfer.tk:6969&tr=udp:%2F%2Ftracker.leechers-paradise.org:6969&tr=udp:%2F%2Fexodus.desync.com:6969");
|
||||
releaseInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:9fb267cff5ae5603f07a347676ec3bf3e35f75e1&dn=Game+of+Thrones+S05E02+HDTV+x264-Xclusive+%5Beztv%5D&tr=udp:%2F%2Fopen.demonii.com:1337&tr=udp:%2F%2Ftracker.coppersurfer.tk:6969&tr=udp:%2F%2Ftracker.leechers-paradise.org:6969&tr=udp:%2F%2Fexodus.desync.com:6969");
|
||||
releaseInfo.InfoUrl.Should().Be("https://thepiratebay.se/torrent/11811366/Series_Title_S05E02_HDTV_x264-Xclusive_%5Beztv%5D");
|
||||
releaseInfo.CommentUrl.Should().Be("https://thepiratebay.se/torrent/11811366/Series_Title_S05E02_HDTV_x264-Xclusive_%5Beztv%5D");
|
||||
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
releaseInfo.PublishDate.Should().Be(DateTime.Parse("Sat, 11 Apr 2015 21:34:00 -0600").ToUniversalTime());
|
||||
releaseInfo.Size.Should().Be(388895872);
|
||||
releaseInfo.InfoHash.Should().Be("9fb267cff5ae5603f07a347676ec3bf3e35f75e1");
|
||||
releaseInfo.Seeders.Should().Be(34128);
|
||||
releaseInfo.Peers.Should().Be(36724);
|
||||
releases.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -13,6 +13,6 @@ namespace NzbDrone.Core.Applications
|
||||
|
||||
public override bool SendUpdatesToClient => true;
|
||||
|
||||
public override string CompletionMessage => null;
|
||||
public override string CompletionMessage => "Completed";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,14 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
|
||||
private readonly ILazyLibrarianV1Proxy _lazyLibrarianV1Proxy;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_lazyLibrarianV1Proxy = lazyLibrarianV1Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -65,7 +67,9 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
{
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
|
||||
@@ -74,7 +78,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
|
||||
_logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol);
|
||||
var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexerCapabilities, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
|
||||
|
||||
@@ -107,11 +111,12 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
|
||||
|
||||
var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol, indexerProps[1]);
|
||||
var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerProps[1]);
|
||||
|
||||
//Use the old remote id to find the indexer on LazyLibrarian incase the update was from a name change in Prowlarr
|
||||
var remoteIndexer = _lazyLibrarianV1Proxy.GetIndexer(indexerProps[1], lazyLibrarianIndexer.Type, Settings);
|
||||
@@ -133,7 +138,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} [{1}] to LazyLibrarian", indexer.Name, indexer.Id);
|
||||
var newRemoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
|
||||
@@ -146,7 +151,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
}
|
||||
}
|
||||
|
||||
private LazyLibrarianIndexer BuildLazyLibrarianIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null)
|
||||
private LazyLibrarianIndexer BuildLazyLibrarianIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, string originalName = null)
|
||||
{
|
||||
var schema = protocol == DownloadProtocol.Usenet ? LazyLibrarianProviderType.Newznab : LazyLibrarianProviderType.Torznab;
|
||||
|
||||
@@ -156,7 +161,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
Altername = $"{indexer.Name} (Prowlarr)",
|
||||
Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api",
|
||||
Apikey = _configFileProvider.ApiKey,
|
||||
Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
|
||||
Categories = string.Join(",", indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
|
||||
Enabled = indexer.Enable,
|
||||
Type = schema,
|
||||
Priority = indexer.Priority
|
||||
|
||||
@@ -21,13 +21,15 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
private readonly ILidarrV1Proxy _lidarrV1Proxy;
|
||||
private readonly ICached<List<LidarrIndexer>> _schemaCache;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public Lidarr(ICacheManager cacheManager, ILidarrV1Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
public Lidarr(ICacheManager cacheManager, ILidarrV1Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_schemaCache = cacheManager.GetCache<List<LidarrIndexer>>(GetType());
|
||||
_lidarrV1Proxy = lidarrV1Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -49,7 +51,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -118,7 +120,9 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
{
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
|
||||
@@ -127,7 +131,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
_logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var lidarrIndexer = BuildLidarrIndexer(indexer, indexer.Protocol);
|
||||
var lidarrIndexer = BuildLidarrIndexer(indexer, indexerCapabilities, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings);
|
||||
|
||||
@@ -159,10 +163,11 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
|
||||
var lidarrIndexer = BuildLidarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
var lidarrIndexer = BuildLidarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
|
||||
var remoteIndexer = _lidarrV1Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings);
|
||||
|
||||
@@ -174,7 +179,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
_logger.Debug("Syncing remote indexer with current settings");
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
lidarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => lidarrIndexer.Fields.All(s => s.Name != f.Name)));
|
||||
@@ -200,7 +205,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Lidarr", indexer.Name, indexer.Id);
|
||||
lidarrIndexer.Id = 0;
|
||||
@@ -214,11 +219,11 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
}
|
||||
}
|
||||
|
||||
private LidarrIndexer BuildLidarrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0)
|
||||
private LidarrIndexer BuildLidarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0)
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
|
||||
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" };
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
@@ -250,7 +255,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
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;
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
|
||||
if (indexer.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
@@ -258,10 +263,15 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
|
||||
if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||
if (lidarrIndexer.Fields.Any(x => x.Name == "seedCriteria.discographySeedTime"))
|
||||
{
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
|
||||
if (lidarrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing"))
|
||||
{
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
}
|
||||
}
|
||||
|
||||
return lidarrIndexer;
|
||||
|
||||
@@ -55,6 +55,10 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
|
||||
var seedRatioCompare = seedRatio == otherSeedRatio;
|
||||
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
|
||||
return other.EnableRss == EnableRss &&
|
||||
other.EnableAutomaticSearch == EnableAutomaticSearch &&
|
||||
other.EnableInteractiveSearch == EnableInteractiveSearch &&
|
||||
@@ -62,7 +66,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
other.Implementation == Implementation &&
|
||||
other.Priority == Priority &&
|
||||
other.Id == Id &&
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,12 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Lidarr in Settings/General")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)]
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -15,12 +15,14 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
|
||||
private readonly IMylarV3Proxy _mylarV3Proxy;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public Mylar(IMylarV3Proxy mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
public Mylar(IMylarV3Proxy mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_mylarV3Proxy = mylarV3Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -65,7 +67,9 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
{
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
|
||||
@@ -74,7 +78,7 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
|
||||
_logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var mylarIndexer = BuildMylarIndexer(indexer, indexer.Protocol);
|
||||
var mylarIndexer = BuildMylarIndexer(indexer, indexerCapabilities, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
|
||||
|
||||
@@ -107,11 +111,12 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
|
||||
|
||||
var mylarIndexer = BuildMylarIndexer(indexer, indexer.Protocol, indexerProps[1]);
|
||||
var mylarIndexer = BuildMylarIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerProps[1]);
|
||||
|
||||
//Use the old remote id to find the indexer on Mylar incase the update was from a name change in Prowlarr
|
||||
var remoteIndexer = _mylarV3Proxy.GetIndexer(indexerProps[1], mylarIndexer.Type, Settings);
|
||||
@@ -133,7 +138,7 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Mylar", indexer.Name, indexer.Id);
|
||||
var newRemoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
|
||||
@@ -146,7 +151,7 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
}
|
||||
}
|
||||
|
||||
private MylarIndexer BuildMylarIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null)
|
||||
private MylarIndexer BuildMylarIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, string originalName = null)
|
||||
{
|
||||
var schema = protocol == DownloadProtocol.Usenet ? MylarProviderType.Newznab : MylarProviderType.Torznab;
|
||||
|
||||
@@ -156,7 +161,7 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
Altername = $"{indexer.Name} (Prowlarr)",
|
||||
Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api",
|
||||
Apikey = _configFileProvider.ApiKey,
|
||||
Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
|
||||
Categories = string.Join(",", indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
|
||||
Enabled = indexer.Enable,
|
||||
Type = schema,
|
||||
};
|
||||
|
||||
@@ -21,13 +21,15 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
private readonly IRadarrV3Proxy _radarrV3Proxy;
|
||||
private readonly ICached<List<RadarrIndexer>> _schemaCache;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_schemaCache = cacheManager.GetCache<List<RadarrIndexer>>(GetType());
|
||||
_radarrV3Proxy = radarrV3Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -49,7 +51,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -118,7 +120,9 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
{
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
|
||||
@@ -127,7 +131,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
_logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var radarrIndexer = BuildRadarrIndexer(indexer, indexer.Protocol);
|
||||
var radarrIndexer = BuildRadarrIndexer(indexer, indexerCapabilities, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings);
|
||||
|
||||
@@ -159,10 +163,11 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
|
||||
var radarrIndexer = BuildRadarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
var radarrIndexer = BuildRadarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
|
||||
var remoteIndexer = _radarrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings);
|
||||
|
||||
@@ -172,7 +177,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
if (!radarrIndexer.Equals(remoteIndexer) || forceSync)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
radarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => radarrIndexer.Fields.All(s => s.Name != f.Name)));
|
||||
@@ -198,7 +203,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Radarr", indexer.Name, indexer.Id);
|
||||
radarrIndexer.Id = 0;
|
||||
@@ -212,11 +217,11 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
}
|
||||
}
|
||||
|
||||
private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0)
|
||||
private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0)
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
|
||||
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" };
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
@@ -248,13 +253,18 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
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;
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
|
||||
if (indexer.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
|
||||
if (radarrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing"))
|
||||
{
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
}
|
||||
}
|
||||
|
||||
return radarrIndexer;
|
||||
|
||||
@@ -51,6 +51,10 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
|
||||
var seedRatioCompare = seedRatio == otherSeedRatio;
|
||||
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
|
||||
return other.EnableRss == EnableRss &&
|
||||
other.EnableAutomaticSearch == EnableAutomaticSearch &&
|
||||
other.EnableInteractiveSearch == EnableInteractiveSearch &&
|
||||
@@ -58,7 +62,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
other.Implementation == Implementation &&
|
||||
other.Priority == Priority &&
|
||||
other.Id == Id &&
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,12 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Radarr in Settings/General")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)]
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -21,13 +21,15 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
private readonly ICached<List<ReadarrIndexer>> _schemaCache;
|
||||
private readonly IReadarrV1Proxy _readarrV1Proxy;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public Readarr(ICacheManager cacheManager, IReadarrV1Proxy readarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
public Readarr(ICacheManager cacheManager, IReadarrV1Proxy readarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_schemaCache = cacheManager.GetCache<List<ReadarrIndexer>>(GetType());
|
||||
_readarrV1Proxy = readarrV1Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -49,7 +51,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_readarrV1Proxy.TestConnection(BuildReadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
failures.AddIfNotNull(_readarrV1Proxy.TestConnection(BuildReadarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -118,7 +120,9 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
{
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
|
||||
@@ -127,7 +131,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
_logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var readarrIndexer = BuildReadarrIndexer(indexer, indexer.Protocol);
|
||||
var readarrIndexer = BuildReadarrIndexer(indexer, indexerCapabilities, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings);
|
||||
|
||||
@@ -159,10 +163,11 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
|
||||
var readarrIndexer = BuildReadarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
var readarrIndexer = BuildReadarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
|
||||
var remoteIndexer = _readarrV1Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings);
|
||||
|
||||
@@ -174,7 +179,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
{
|
||||
_logger.Debug("Syncing remote indexer with current settings");
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
readarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => readarrIndexer.Fields.All(s => s.Name != f.Name)));
|
||||
@@ -200,7 +205,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Readarr", indexer.Name, indexer.Id);
|
||||
readarrIndexer.Id = 0;
|
||||
@@ -214,11 +219,11 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
}
|
||||
}
|
||||
|
||||
private ReadarrIndexer BuildReadarrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0)
|
||||
private ReadarrIndexer BuildReadarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0)
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
|
||||
var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" };
|
||||
|
||||
var newznab = schemas.First(i => i.Implementation == "Newznab");
|
||||
var torznab = schemas.First(i => i.Implementation == "Torznab");
|
||||
@@ -244,7 +249,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
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;
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
|
||||
if (indexer.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
@@ -252,10 +257,15 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
|
||||
if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||
if (readarrIndexer.Fields.Any(x => x.Name == "seedCriteria.discographySeedTime"))
|
||||
{
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
|
||||
if (readarrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing"))
|
||||
{
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
}
|
||||
}
|
||||
|
||||
return readarrIndexer;
|
||||
|
||||
@@ -55,6 +55,10 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
|
||||
var seedRatioCompare = seedRatio == otherSeedRatio;
|
||||
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
|
||||
return other.EnableRss == EnableRss &&
|
||||
other.EnableAutomaticSearch == EnableAutomaticSearch &&
|
||||
other.EnableInteractiveSearch == EnableInteractiveSearch &&
|
||||
@@ -62,7 +66,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
other.Implementation == Implementation &&
|
||||
other.Priority == Priority &&
|
||||
other.Id == Id &&
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,12 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Readarr in Settings/General")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)]
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -21,13 +21,15 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
private readonly ICached<List<SonarrIndexer>> _schemaCache;
|
||||
private readonly ISonarrV3Proxy _sonarrV3Proxy;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_schemaCache = cacheManager.GetCache<List<SonarrIndexer>>(GetType());
|
||||
_sonarrV3Proxy = sonarrV3Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -49,7 +51,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -122,8 +124,10 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty() &&
|
||||
indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Empty())
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty() &&
|
||||
indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Empty())
|
||||
{
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
|
||||
@@ -132,7 +136,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
_logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var sonarrIndexer = BuildSonarrIndexer(indexer, indexer.Protocol);
|
||||
var sonarrIndexer = BuildSonarrIndexer(indexer, indexerCapabilities, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings);
|
||||
|
||||
@@ -164,10 +168,11 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
|
||||
var sonarrIndexer = BuildSonarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
var sonarrIndexer = BuildSonarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
|
||||
var remoteIndexer = _sonarrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings);
|
||||
|
||||
@@ -179,7 +184,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
{
|
||||
_logger.Debug("Syncing remote indexer with current settings");
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
sonarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => sonarrIndexer.Fields.All(s => s.Name != f.Name)));
|
||||
@@ -206,7 +211,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Sonarr", indexer.Name, indexer.Id);
|
||||
sonarrIndexer.Id = 0;
|
||||
@@ -220,11 +225,11 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
}
|
||||
}
|
||||
|
||||
private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0)
|
||||
private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0)
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "animeStandardFormatSearch", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
|
||||
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "animeStandardFormatSearch", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" };
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
@@ -256,8 +261,8 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
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;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()));
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()));
|
||||
|
||||
if (sonarrIndexer.Fields.Any(x => x.Name == "animeStandardFormatSearch"))
|
||||
{
|
||||
@@ -270,6 +275,11 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
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.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
|
||||
if (sonarrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing"))
|
||||
{
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
}
|
||||
}
|
||||
|
||||
return sonarrIndexer;
|
||||
|
||||
@@ -61,6 +61,10 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
|
||||
var seedRatioCompare = seedRatio == otherSeedRatio;
|
||||
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
|
||||
return other.EnableRss == EnableRss &&
|
||||
other.EnableAutomaticSearch == EnableAutomaticSearch &&
|
||||
other.EnableInteractiveSearch == EnableInteractiveSearch &&
|
||||
@@ -68,7 +72,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
other.Implementation == Implementation &&
|
||||
other.Priority == Priority &&
|
||||
other.Id == Id &&
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && animeCats && animeStandardFormatSearchCompare && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && animeCats && animeStandardFormatSearchCompare && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +43,12 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
[FieldDefinition(4, Label = "Anime Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
|
||||
public IEnumerable<int> AnimeSyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Sync Anime Standard Format Search", Type = FieldType.Checkbox, Advanced = true, HelpText = "Sync also searching for anime using the standard numbering")]
|
||||
[FieldDefinition(5, Label = "Sync Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Sync also searching for anime using the standard numbering", Advanced = true)]
|
||||
public bool SyncAnimeStandardFormatSearch { get; set; }
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -21,13 +21,15 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
private readonly IWhisparrV3Proxy _whisparrV3Proxy;
|
||||
private readonly ICached<List<WhisparrIndexer>> _schemaCache;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_schemaCache = cacheManager.GetCache<List<WhisparrIndexer>>(GetType());
|
||||
_whisparrV3Proxy = whisparrV3Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -49,7 +51,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_whisparrV3Proxy.TestConnection(BuildWhisparrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
failures.AddIfNotNull(_whisparrV3Proxy.TestConnection(BuildWhisparrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -118,7 +120,9 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
{
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
|
||||
@@ -127,7 +131,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
_logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var whisparrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol);
|
||||
var whisparrIndexer = BuildWhisparrIndexer(indexer, indexerCapabilities, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _whisparrV3Proxy.AddIndexer(whisparrIndexer, Settings);
|
||||
|
||||
@@ -159,10 +163,11 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var indexerCapabilities = _indexerFactory.GetInstance(indexer).GetCapabilities();
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
|
||||
var whisparrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
var whisparrIndexer = BuildWhisparrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
|
||||
var remoteIndexer = _whisparrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings);
|
||||
|
||||
@@ -174,7 +179,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
_logger.Debug("Syncing remote indexer with current settings");
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
whisparrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => whisparrIndexer.Fields.All(s => s.Name != f.Name)));
|
||||
@@ -200,7 +205,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Whisparr", indexer.Name, indexer.Id);
|
||||
whisparrIndexer.Id = 0;
|
||||
@@ -214,11 +219,11 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
}
|
||||
}
|
||||
|
||||
private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0)
|
||||
private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0)
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
|
||||
var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" };
|
||||
|
||||
var newznab = schemas.First(i => i.Implementation == "Newznab");
|
||||
var torznab = schemas.First(i => i.Implementation == "Torznab");
|
||||
@@ -244,13 +249,23 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
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;
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
|
||||
if (indexer.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
|
||||
if (whisparrIndexer.Fields.Any(x => x.Name == "seedCriteria.seasonPackSeedTime"))
|
||||
{
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
|
||||
if (whisparrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing"))
|
||||
{
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
}
|
||||
}
|
||||
|
||||
return whisparrIndexer;
|
||||
|
||||
@@ -47,10 +47,18 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
var otherSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value);
|
||||
var seedTimeCompare = seedTime == otherSeedTime;
|
||||
|
||||
var seasonSeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
|
||||
var otherSeasonSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
|
||||
var seasonSeedTimeCompare = seasonSeedTime == otherSeasonSeedTime;
|
||||
|
||||
var seedRatio = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
|
||||
var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
|
||||
var seedRatioCompare = seedRatio == otherSeedRatio;
|
||||
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value);
|
||||
var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing;
|
||||
|
||||
return other.EnableRss == EnableRss &&
|
||||
other.EnableAutomaticSearch == EnableAutomaticSearch &&
|
||||
other.EnableInteractiveSearch == EnableInteractiveSearch &&
|
||||
@@ -58,7 +66,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
other.Implementation == Implementation &&
|
||||
other.Priority == Priority &&
|
||||
other.Id == Id &&
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
|
||||
apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,12 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Whisparr in Settings/General")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)]
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(40)]
|
||||
public class newznab_category_to_capabilities_settings : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.WithConnection(MoveCategoriesToCapabilities);
|
||||
}
|
||||
|
||||
private void MoveCategoriesToCapabilities(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updated = new List<object>();
|
||||
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" IN ('Newznab', 'Torznab')";
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var settings = Json.Deserialize<JObject>(reader.GetString(1));
|
||||
|
||||
if ((settings.Value<JObject>("capabilities")?.ContainsKey("categories") ?? false) == false
|
||||
&& settings.ContainsKey("categories")
|
||||
&& settings.TryGetValue("categories", out var categories))
|
||||
{
|
||||
if (!settings.ContainsKey("capabilities"))
|
||||
{
|
||||
settings.Add("capabilities", new JObject());
|
||||
}
|
||||
|
||||
settings.Value<JObject>("capabilities")?.Add(new JProperty("categories", JArray.FromObject(categories)));
|
||||
|
||||
if (settings.ContainsKey("categories"))
|
||||
{
|
||||
settings.Remove("categories");
|
||||
}
|
||||
}
|
||||
|
||||
updated.Add(new
|
||||
{
|
||||
Settings = settings.ToJson(),
|
||||
Id = id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updateSql = "UPDATE \"Indexers\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id";
|
||||
conn.Execute(updateSql, updated, transaction: tran);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,14 +308,15 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
return Settings.TvDirectory.TrimStart('/');
|
||||
}
|
||||
else if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var destDir = GetDefaultDir();
|
||||
|
||||
var destDir = GetDefaultDir();
|
||||
|
||||
if (destDir.IsNotNullOrWhiteSpace() && Settings.Category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.Category}";
|
||||
}
|
||||
|
||||
return null;
|
||||
return destDir.TrimEnd('/');
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink)
|
||||
|
||||
@@ -274,14 +274,15 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
return Settings.TvDirectory.TrimStart('/');
|
||||
}
|
||||
else if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var destDir = GetDefaultDir();
|
||||
|
||||
var destDir = GetDefaultDir();
|
||||
|
||||
if (destDir.IsNotNullOrWhiteSpace() && Settings.Category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.Category}";
|
||||
}
|
||||
|
||||
return null;
|
||||
return destDir.TrimEnd('/');
|
||||
}
|
||||
|
||||
protected override string AddFromLink(ReleaseInfo release)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Download.Clients.Sabnzbd.JsonConverters;
|
||||
@@ -7,10 +7,14 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
{
|
||||
public class SabnzbdConfig
|
||||
{
|
||||
public SabnzbdConfig()
|
||||
{
|
||||
Categories = new List<SabnzbdCategory>();
|
||||
Servers = new List<object>();
|
||||
}
|
||||
|
||||
public SabnzbdConfigMisc Misc { get; set; }
|
||||
|
||||
public List<SabnzbdCategory> Categories { get; set; }
|
||||
|
||||
public List<object> Servers { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NLog;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
@@ -28,7 +29,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
_logger.Warn("Please update your API key to be at least {0} characters long. You can do this via settings or the config file", MinimumLength);
|
||||
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage"), MinimumLength), "#invalid-api-key");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage", new Dictionary<string, object> { { "length", MinimumLength } }), "#invalid-api-key");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Download;
|
||||
@@ -39,10 +40,19 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
if (backOffProviders.Count == enabledProviders.Count)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("DownloadClientStatusCheckAllClientMessage"), "#download-clients-are-unavailable-due-to-failures");
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("DownloadClientStatusAllClientHealthCheckMessage"),
|
||||
"#download-clients-are-unavailable-due-to-failures");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#download-clients-are-unavailable-due-to-failures");
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
_localizationService.GetLocalizedString("DownloadClientStatusSingleClientHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "downloadClientNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
|
||||
}),
|
||||
"#download-clients-are-unavailable-due-to-failures");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Indexers;
|
||||
@@ -39,7 +40,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage"), string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray())),
|
||||
_localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "indexerNames", string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray()) }
|
||||
}),
|
||||
"#invalid-indexer-download-client-setting");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
@@ -17,9 +18,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
private readonly IIndexerFactory _providerFactory;
|
||||
private readonly IIndexerStatusService _providerStatusService;
|
||||
|
||||
public IndexerLongTermStatusCheck(IIndexerFactory providerFactory,
|
||||
IIndexerStatusService providerStatusService,
|
||||
ILocalizationService localizationService)
|
||||
public IndexerLongTermStatusCheck(IIndexerFactory providerFactory, IIndexerStatusService providerStatusService, ILocalizationService localizationService)
|
||||
: base(localizationService)
|
||||
{
|
||||
_providerFactory = providerFactory;
|
||||
@@ -46,14 +45,16 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("IndexerLongTermStatusCheckAllClientMessage"),
|
||||
_localizationService.GetLocalizedString("IndexerLongTermStatusAllUnavailableHealthCheckMessage"),
|
||||
"#indexers-are-unavailable-due-to-failures");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage"),
|
||||
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
|
||||
_localizationService.GetLocalizedString("IndexerLongTermStatusUnavailableHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
|
||||
}),
|
||||
"#indexers-are-unavailable-due-to-failures");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.IndexerProxies;
|
||||
@@ -9,11 +10,11 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexerProxy>))]
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexerProxy>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexerProxy>))]
|
||||
public class IndexerProxyCheck : HealthCheckBase
|
||||
public class IndexerProxyStatusCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerProxyFactory _proxyFactory;
|
||||
|
||||
public IndexerProxyCheck(IIndexerProxyFactory proxyFactory,
|
||||
public IndexerProxyStatusCheck(IIndexerProxyFactory proxyFactory,
|
||||
ILocalizationService localizationService)
|
||||
: base(localizationService)
|
||||
{
|
||||
@@ -37,15 +38,17 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("IndexerProxyStatusCheckAllClientMessage"),
|
||||
"#proxies-are-unavailable-due-to-failures");
|
||||
_localizationService.GetLocalizedString("IndexerProxyStatusAllUnavailableHealthCheckMessage"),
|
||||
"#indexer-proxies-are-unavailable-due-to-failures");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerProxyStatusCheckSingleClientMessage"),
|
||||
string.Join(", ", badProxies.Select(v => v.Definition.Name))),
|
||||
"#proxies-are-unavailable-due-to-failures");
|
||||
_localizationService.GetLocalizedString("IndexerProxyStatusUnavailableHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "indexerProxyNames", string.Join(", ", badProxies.Select(v => v.Definition.Name)) }
|
||||
}),
|
||||
"#indexer-proxies-are-unavailable-due-to-failures");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
@@ -44,14 +45,16 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("IndexerStatusCheckAllClientMessage"),
|
||||
_localizationService.GetLocalizedString("IndexerStatusAllUnavailableHealthCheckMessage"),
|
||||
"#indexers-are-unavailable-due-to-failures");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage"),
|
||||
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
|
||||
_localizationService.GetLocalizedString("IndexerStatusUnavailableHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
|
||||
}),
|
||||
"#indexers-are-unavailable-due-to-failures");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class IndexerVIPCheck : HealthCheckBase
|
||||
{
|
||||
@@ -24,7 +25,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var indexers = _indexerFactory.AllProviders(false);
|
||||
var indexers = _indexerFactory.Enabled(false);
|
||||
var expiringProviders = new List<IIndexer>();
|
||||
|
||||
foreach (var provider in indexers)
|
||||
@@ -39,12 +40,8 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
var expiration = (string)vipProp.GetValue(provider.Definition.Settings);
|
||||
|
||||
if (expiration.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DateTime.Parse(expiration).Between(DateTime.Now, DateTime.Now.AddDays(7)))
|
||||
if (expiration.IsNotNullOrWhiteSpace() &&
|
||||
DateTime.Parse(expiration).Between(DateTime.Now, DateTime.Now.AddDays(7)))
|
||||
{
|
||||
expiringProviders.Add(provider);
|
||||
}
|
||||
@@ -53,10 +50,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
if (!expiringProviders.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiringClientMessage"),
|
||||
string.Join(", ", expiringProviders.Select(v => v.Definition.Name))),
|
||||
"#indexer-vip-expiring");
|
||||
HealthCheckResult.Warning,
|
||||
_localizationService.GetLocalizedString("IndexerVipExpiringHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "indexerNames", string.Join(", ", expiringProviders.Select(v => v.Definition.Name).ToArray()) }
|
||||
}),
|
||||
"#indexer-vip-expiring");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class IndexerVIPExpiredCheck : HealthCheckBase
|
||||
{
|
||||
@@ -24,7 +25,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var indexers = _indexerFactory.AllProviders(false);
|
||||
var indexers = _indexerFactory.Enabled(false);
|
||||
var expiredProviders = new List<IIndexer>();
|
||||
|
||||
foreach (var provider in indexers)
|
||||
@@ -39,12 +40,8 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
var expiration = (string)vipProp.GetValue(provider.Definition.Settings);
|
||||
|
||||
if (expiration.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DateTime.Parse(expiration).Before(DateTime.Now))
|
||||
if (expiration.IsNotNullOrWhiteSpace() &&
|
||||
DateTime.Parse(expiration).Before(DateTime.Now))
|
||||
{
|
||||
expiredProviders.Add(provider);
|
||||
}
|
||||
@@ -53,10 +50,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
if (!expiredProviders.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiredClientMessage"),
|
||||
string.Join(", ", expiredProviders.Select(v => v.Definition.Name))),
|
||||
"#indexer-vip-expired");
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("IndexerVipExpiredHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "indexerNames", string.Join(", ", expiredProviders.Select(v => v.Definition.Name).ToArray()) }
|
||||
}),
|
||||
"#indexer-vip-expired");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Localization;
|
||||
@@ -45,7 +46,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
|
||||
_localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "notificationNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
|
||||
}),
|
||||
"#notifications-are-unavailable-due-to-failures");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
@@ -19,7 +20,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
private readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
|
||||
|
||||
public ProxyCheck(IProwlarrCloudRequestBuilder cloudRequestBuilder, IConfigService configService, IHttpClient client, ILocalizationService localizationService, Logger logger)
|
||||
public ProxyCheck(IProwlarrCloudRequestBuilder cloudRequestBuilder, IConfigService configService, IHttpClient client, Logger logger, ILocalizationService localizationService)
|
||||
: base(localizationService)
|
||||
{
|
||||
_configService = configService;
|
||||
@@ -31,35 +32,58 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
if (_configService.ProxyEnabled)
|
||||
if (!_configService.ProxyEnabled)
|
||||
{
|
||||
var addresses = Dns.GetHostAddresses(_configService.ProxyHostname);
|
||||
if (!addresses.Any())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), _configService.ProxyHostname));
|
||||
}
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build();
|
||||
var addresses = Dns.GetHostAddresses(_configService.ProxyHostname);
|
||||
|
||||
try
|
||||
{
|
||||
var response = _client.Execute(request);
|
||||
|
||||
// We only care about 400 responses, other error codes can be ignored
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
if (!addresses.Any())
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("ProxyResolveIpHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{ "proxyHostName", _configService.ProxyHostname }
|
||||
}),
|
||||
"#proxy-failed-resolve-ip");
|
||||
}
|
||||
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
var response = _client.Execute(request);
|
||||
|
||||
// We only care about 400 responses, other error codes can be ignored
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url));
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("ProxyBadRequestHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "statusCode", response.StatusCode }
|
||||
}),
|
||||
"#proxy-failed-test");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("ProxyFailedToTestHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "url", request.Url }
|
||||
}),
|
||||
"#proxy-failed-test");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
@@ -47,7 +48,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupTranslocationMessage"), startupFolder),
|
||||
_localizationService.GetLocalizedString(
|
||||
"UpdateStartupTranslocationHealthCheckMessage",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "startupFolder", startupFolder }
|
||||
}),
|
||||
"#cannot-install-update-because-startup-folder-is-in-an-app-translocation-folder.");
|
||||
}
|
||||
|
||||
@@ -55,7 +61,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupNotWritableMessage"), startupFolder, Environment.UserName),
|
||||
_localizationService.GetLocalizedString(
|
||||
"UpdateStartupNotWritableHealthCheckMessage",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "startupFolder", startupFolder },
|
||||
{ "userName", Environment.UserName }
|
||||
}),
|
||||
"#cannot-install-update-because-startup-folder-is-not-writable-by-the-user");
|
||||
}
|
||||
|
||||
@@ -63,14 +75,20 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
string.Format(_localizationService.GetLocalizedString("UpdateCheckUINotWritableMessage"), uiFolder, Environment.UserName),
|
||||
_localizationService.GetLocalizedString(
|
||||
"UpdateUiNotWritableHealthCheckMessage",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "uiFolder", uiFolder },
|
||||
{ "userName", Environment.UserName }
|
||||
}),
|
||||
"#cannot-install-update-because-ui-folder-is-not-writable-by-the-user");
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14) && _checkUpdateService.AvailableUpdate() != null)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("UpdateAvailable"), "#new-update-is-available");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("UpdateAvailableHealthCheckMessage"));
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -102,9 +102,15 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
var url = request.Url.ToString();
|
||||
var maxTimeout = Settings.RequestTimeout * 1000;
|
||||
|
||||
// Use Proxy if no credentials are set (creds not supported as of FS 2.2.9)
|
||||
var proxySettings = _proxySettingsProvider.GetProxySettings();
|
||||
var proxyUrl = proxySettings != null && proxySettings.Username.IsNullOrWhiteSpace() && proxySettings.Password.IsNullOrWhiteSpace() ? GetProxyUri(proxySettings) : null;
|
||||
var proxyUrl = proxySettings != null ? GetProxyUri(proxySettings) : null;
|
||||
|
||||
var requestProxy = new FlareSolverrProxy
|
||||
{
|
||||
Url = proxyUrl?.OriginalString,
|
||||
Username = proxySettings != null && proxySettings.Username.IsNotNullOrWhiteSpace() ? proxySettings.Username : null,
|
||||
Password = proxySettings != null && proxySettings.Password.IsNotNullOrWhiteSpace() ? proxySettings.Password : null
|
||||
};
|
||||
|
||||
if (request.Method == HttpMethod.Get)
|
||||
{
|
||||
@@ -113,10 +119,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
Cmd = "request.get",
|
||||
Url = url,
|
||||
MaxTimeout = maxTimeout,
|
||||
Proxy = new FlareSolverrProxy
|
||||
{
|
||||
Url = proxyUrl?.OriginalString
|
||||
}
|
||||
Proxy = requestProxy
|
||||
};
|
||||
}
|
||||
else if (request.Method == HttpMethod.Post)
|
||||
@@ -139,10 +142,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
ContentLength = null
|
||||
},
|
||||
MaxTimeout = maxTimeout,
|
||||
Proxy = new FlareSolverrProxy
|
||||
{
|
||||
Url = proxyUrl?.OriginalString
|
||||
}
|
||||
Proxy = requestProxy
|
||||
};
|
||||
}
|
||||
else if (contentTypeType.Contains("multipart/form-data")
|
||||
@@ -169,6 +169,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
newRequest.LogResponseContent = true;
|
||||
newRequest.RequestTimeout = TimeSpan.FromSeconds(Settings.RequestTimeout + 5);
|
||||
newRequest.SetContent(req.ToJson());
|
||||
newRequest.ContentSummary = req.ToJson(Formatting.None);
|
||||
|
||||
_logger.Debug("Cloudflare Detected, Applying FlareSolverr Proxy {0} to request {1}", Name, request.Url);
|
||||
|
||||
@@ -243,6 +244,8 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
private class FlareSolverrProxy
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
private class HeadersPost
|
||||
|
||||
@@ -177,8 +177,8 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
if (criteriaBase.Categories is { Length: > 0 })
|
||||
{
|
||||
//Only query supported indexers
|
||||
indexers = indexers.Where(i => ((IndexerDefinition)i.Definition).Capabilities.Categories.SupportedCategories(criteriaBase.Categories).Any()).ToList();
|
||||
// Only query supported indexers
|
||||
indexers = indexers.Where(i => i.GetCapabilities().Categories.SupportedCategories(criteriaBase.Categories).Any()).ToList();
|
||||
|
||||
if (indexers.Count == 0)
|
||||
{
|
||||
@@ -196,7 +196,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
var reports = batch.SelectMany(x => x).ToList();
|
||||
|
||||
_logger.Debug("Total of {0} reports were found for {1} from {2} indexer(s)", reports.Count, criteriaBase, indexers.Count);
|
||||
_logger.ProgressDebug("Total of {0} reports were found for {1} from {2} indexer(s)", reports.Count, criteriaBase, indexers.Count);
|
||||
|
||||
return reports;
|
||||
}
|
||||
@@ -217,7 +217,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
//Filter results to only those in searched categories
|
||||
if (criteriaBase.Categories.Length > 0)
|
||||
{
|
||||
var expandedQueryCats = ((IndexerDefinition)indexer.Definition).Capabilities.Categories.ExpandTorznabQueryCategories(criteriaBase.Categories);
|
||||
var expandedQueryCats = indexer.GetCapabilities().Categories.ExpandTorznabQueryCategories(criteriaBase.Categories);
|
||||
|
||||
releases = releases.Where(result => result.Categories?.Any() != true || expandedQueryCats.Intersect(result.Categories.Select(c => c.Id)).Any()).ToList();
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
"desitorrents",
|
||||
"hdbits",
|
||||
"lat-team",
|
||||
"mteamtp",
|
||||
"mteamtp2fa",
|
||||
"reelflix",
|
||||
"shareisland",
|
||||
"skipthecommercials",
|
||||
@@ -137,11 +139,11 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
if (directoryInfo.Exists)
|
||||
{
|
||||
var files = directoryInfo.GetFiles($"*.yml", options);
|
||||
var files = directoryInfo.GetFiles("*.yml", options);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
_logger.Debug("Loading definition " + file.FullName);
|
||||
_logger.Debug("Loading definition {0}", file.FullName);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -158,9 +160,9 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
indexerList.Add(definition);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error while parsing Cardigann definition {file.FullName}\n{e}");
|
||||
_logger.Error(ex, "Error while parsing Cardigann definition {0}", file.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +190,8 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
if (files.Any())
|
||||
{
|
||||
var file = files.First();
|
||||
_logger.Trace("Loading Cardigann definition " + file.FullName);
|
||||
_logger.Trace("Loading Cardigann definition {0}", file.FullName);
|
||||
|
||||
try
|
||||
{
|
||||
var definitionString = File.ReadAllText(file.FullName);
|
||||
@@ -196,9 +199,9 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
return CleanIndexerDefinition(definition);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error while parsing Cardigann definition {file.FullName}\n{e}");
|
||||
_logger.Error(ex, "Error while parsing Cardigann definition {0}", file.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,7 +209,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
var dbDefs = _versionService.All();
|
||||
|
||||
//Check to ensure it's in versioned defs before we go to web
|
||||
if (dbDefs.Count > 0 && !dbDefs.Any(x => x.File == fileKey))
|
||||
if (dbDefs.Count > 0 && dbDefs.All(x => x.File != fileKey))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileKey));
|
||||
}
|
||||
@@ -217,9 +220,10 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
private CardigannDefinition GetHttpDefinition(string id)
|
||||
{
|
||||
var req = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}");
|
||||
var response = _httpClient.Get(req);
|
||||
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}");
|
||||
var response = _httpClient.Get(request);
|
||||
var definition = _deserializer.Deserialize<CardigannDefinition>(response.Content);
|
||||
|
||||
return CleanIndexerDefinition(definition);
|
||||
}
|
||||
|
||||
@@ -289,7 +293,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
EnsureDefinitionsFolder();
|
||||
|
||||
var definitionsFolder = Path.Combine(startupFolder, "Definitions");
|
||||
var saveFile = Path.Combine(definitionsFolder, $"indexers.zip");
|
||||
var saveFile = Path.Combine(definitionsFolder, "indexers.zip");
|
||||
|
||||
_httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/package.zip", saveFile);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
@@ -43,7 +44,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnidubParser(Settings, Capabilities.Categories, RateLimit, _httpClient, _logger);
|
||||
return new AnidubParser(Definition, Settings, Capabilities.Categories, RateLimit, _httpClient, _logger);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
@@ -244,6 +245,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class AnidubParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly ProviderDefinition _definition;
|
||||
private readonly UserPassTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly TimeSpan _rateLimit;
|
||||
@@ -270,8 +272,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{ "/anons_ongoing", "12" }
|
||||
};
|
||||
|
||||
public AnidubParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger)
|
||||
public AnidubParser(ProviderDefinition definition, UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_definition = definition;
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_rateLimit = rateLimit;
|
||||
@@ -479,7 +482,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
.Build();
|
||||
|
||||
var releaseIndexerRequest = new IndexerRequest(releaseRequest);
|
||||
var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.Execute(releaseIndexerRequest.HttpRequest));
|
||||
var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.ExecuteProxied(releaseIndexerRequest.HttpRequest, _definition));
|
||||
|
||||
// Throw common http errors here before we try to parse
|
||||
if (releaseResponse.HttpResponse.HasHttpError)
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
@@ -209,6 +209,24 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
if (searchType == "music" && searchCriteria is MusicSearchCriteria musicSearchCriteria)
|
||||
{
|
||||
if (musicSearchCriteria.Artist.IsNotNullOrWhiteSpace() && musicSearchCriteria.Artist != "VA")
|
||||
{
|
||||
parameters.Set("artistnames", musicSearchCriteria.Artist);
|
||||
}
|
||||
|
||||
if (musicSearchCriteria.Album.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("groupname", musicSearchCriteria.Album);
|
||||
}
|
||||
|
||||
if (musicSearchCriteria.Year is > 0)
|
||||
{
|
||||
parameters.Set("year", musicSearchCriteria.Year.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
|
||||
if (queryCats.Any())
|
||||
|
||||
@@ -14,6 +14,7 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
@@ -40,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnimediaParser(Settings, Capabilities.Categories, RateLimit, _httpClient);
|
||||
return new AnimediaParser(Definition, Settings, Capabilities.Categories, RateLimit, _httpClient);
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -144,6 +145,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class AnimediaParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly ProviderDefinition _definition;
|
||||
private readonly NoAuthTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly TimeSpan _rateLimit;
|
||||
@@ -157,8 +159,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
private static readonly Regex CategorieOVARegex = new Regex(@"ОВА|OVA|ОНА|ONA|Special", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex CategorieDoramaRegex = new Regex(@"Дорама", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public AnimediaParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient)
|
||||
public AnimediaParser(ProviderDefinition definition, NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient)
|
||||
{
|
||||
_definition = definition;
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_rateLimit = rateLimit;
|
||||
@@ -311,7 +314,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
.Build();
|
||||
|
||||
var releaseIndexerRequest = new IndexerRequest(releaseRequest);
|
||||
var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.Execute(releaseIndexerRequest.HttpRequest));
|
||||
var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.ExecuteProxied(releaseIndexerRequest.HttpRequest, _definition));
|
||||
|
||||
// Throw common http errors here before we try to parse
|
||||
if (releaseResponse.HttpResponse.HasHttpError)
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
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;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
[Obsolete("Moved to YML for Cardigann")]
|
||||
public class Anthelion : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "Anthelion";
|
||||
public override string[] IndexerUrls => new string[] { "https://anthelion.me/" };
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override string Description => "A movies tracker";
|
||||
public override string Language => "en-US";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Anthelion(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnthelionRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnthelionParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
var cookies = Cookies;
|
||||
Cookies = null;
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("keeplogged", "1")
|
||||
.AddFormParameter("login", "Log+In!")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.SetHeader("Referer", LoginUrl)
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("form#loginform")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
}
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("Anthelion authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return !httpResponse.Content.Contains("logout.php");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Film/Feature");
|
||||
caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.Movies, "Film/Short");
|
||||
caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.TV, "TV/Miniseries");
|
||||
caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.Other, "Other");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnthelionRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public UserPassTorrentBaseSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
|
||||
{
|
||||
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
|
||||
|
||||
// TODO: IMDB search is available but it requires to parse the details page
|
||||
var qc = new NameValueCollection
|
||||
{
|
||||
{ "order_by", "time" },
|
||||
{ "order_way", "desc" },
|
||||
{ "action", "basic" },
|
||||
{ "searchsubmit", "1" },
|
||||
{ "searchstr", imdbId.IsNotNullOrWhiteSpace() ? imdbId : term.Replace(".", " ") }
|
||||
};
|
||||
|
||||
var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
|
||||
foreach (var cat in catList)
|
||||
{
|
||||
qc.Add($"filter_cat[{cat}]", "1");
|
||||
}
|
||||
|
||||
searchUrl = searchUrl + "?" + qc.GetQueryString();
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AnthelionParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly UserPassTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public AnthelionParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
using var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = doc.QuerySelectorAll("table.torrent_table > tbody > tr.torrent");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var qDetailsLink = row.QuerySelector("a.torrent_name");
|
||||
var year = qDetailsLink.NextSibling.TextContent.Replace("[", "").Replace("]", "").Trim();
|
||||
var tags = row.QuerySelector("div.torrent_info").FirstChild.TextContent.Replace(" / ", " ").Trim();
|
||||
var title = $"{qDetailsLink.TextContent} {year} {tags}";
|
||||
var description = row.QuerySelector("div.tags").TextContent.Trim();
|
||||
var details = _settings.BaseUrl + qDetailsLink.GetAttribute("href");
|
||||
var torrentId = qDetailsLink.GetAttribute("href").Split('=').Last();
|
||||
var link = _settings.BaseUrl + "torrents.php?action=download&id=" + torrentId;
|
||||
var posterStr = qDetailsLink.GetAttribute("data-cover");
|
||||
var poster = !string.IsNullOrWhiteSpace(posterStr) ? posterStr : null;
|
||||
|
||||
var files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(3)").TextContent);
|
||||
var publishDate = DateTimeUtil.FromTimeAgo(row.QuerySelector("td:nth-child(4)").TextContent);
|
||||
var size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(5)").FirstChild.TextContent);
|
||||
var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent);
|
||||
var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent);
|
||||
var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(8)").TextContent);
|
||||
|
||||
var dlVolumeFactor = row.QuerySelector("strong.tl_free") != null ? 0 : 1;
|
||||
|
||||
var cat = row.QuerySelector("td.cats_col > div").GetAttribute("class").Replace("tooltip cats_", "");
|
||||
var category = new List<IndexerCategory>
|
||||
{
|
||||
cat switch
|
||||
{
|
||||
"featurefilm" => NewznabStandardCategory.Movies,
|
||||
"shortfilm" => NewznabStandardCategory.Movies,
|
||||
"miniseries" => NewznabStandardCategory.TV,
|
||||
"other" => NewznabStandardCategory.Other,
|
||||
_ => throw new Exception($"Unknown category: {cat}")
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: TMDb is also available
|
||||
var qImdb = row.QuerySelector("a[href^=\"https://www.imdb.com\"]");
|
||||
var imdb = qImdb != null ? ParseUtil.GetImdbId(qImdb.GetAttribute("href").Split('/').Last()) : null;
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 259200,
|
||||
Description = description,
|
||||
Title = title,
|
||||
PublishDate = publishDate,
|
||||
Categories = category,
|
||||
DownloadUrl = link,
|
||||
InfoUrl = details,
|
||||
PosterUrl = poster,
|
||||
Guid = link,
|
||||
ImdbId = imdb.GetValueOrDefault(),
|
||||
Seeders = seeders,
|
||||
Peers = leechers + seeders,
|
||||
Size = size,
|
||||
Grabs = grabs,
|
||||
Files = files,
|
||||
DownloadVolumeFactor = dlVolumeFactor,
|
||||
UploadVolumeFactor = 1
|
||||
};
|
||||
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions;
|
||||
|
||||
[Obsolete("User Agent blocked")]
|
||||
public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "AudioBook Bay";
|
||||
public override string[] IndexerUrls => new[]
|
||||
{
|
||||
"https://audiobookbay.is/"
|
||||
};
|
||||
public override string[] LegacyUrls => new[]
|
||||
{
|
||||
"https://audiobookbay.la/",
|
||||
"http://audiobookbay.net/",
|
||||
"https://audiobookbay.unblockit.tv/",
|
||||
"http://audiobookbay.nl/",
|
||||
"http://audiobookbay.ws/",
|
||||
"https://audiobookbay.unblockit.how/",
|
||||
"https://audiobookbay.unblockit.cam/",
|
||||
"https://audiobookbay.unblockit.biz/",
|
||||
"https://audiobookbay.unblockit.day/",
|
||||
"https://audiobookbay.unblockit.llc/",
|
||||
"https://audiobookbay.unblockit.blue/",
|
||||
"https://audiobookbay.unblockit.name/",
|
||||
"http://audiobookbay.fi/",
|
||||
"http://audiobookbay.se/",
|
||||
"http://audiobookbayabb.com/",
|
||||
"https://audiobookbay.unblockit.ist/",
|
||||
"https://audiobookbay.unblockit.bet/",
|
||||
"https://audiobookbay.unblockit.cat/",
|
||||
"https://audiobookbay.unblockit.nz/",
|
||||
"https://audiobookbay.fi/",
|
||||
"https://audiobookbay.unblockit.page/",
|
||||
"https://audiobookbay.unblockit.pet/",
|
||||
"https://audiobookbay.unblockit.ink/",
|
||||
"https://audiobookbay.unblockit.bio/", // error 502
|
||||
"https://audiobookbay.li/",
|
||||
"https://audiobookbay.se/" // redirects to .is but has invalid CA
|
||||
};
|
||||
public override string Description => "AudioBook Bay (ABB) is a public Torrent Tracker for AUDIOBOOKS";
|
||||
public override string Language => "en-US";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
|
||||
public override int PageSize => 15;
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5);
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public AudioBookBay(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AudioBookBayRequestGenerator(Settings);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AudioBookBayParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
{
|
||||
var request = new HttpRequestBuilder(link.ToString())
|
||||
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
|
||||
.Accept(HttpAccept.Html)
|
||||
.Build();
|
||||
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
var parser = new HtmlParser();
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
|
||||
var hash = dom.QuerySelector("td:contains(\"Info Hash:\") ~ td")?.TextContent.Trim();
|
||||
if (hash == null)
|
||||
{
|
||||
throw new Exception($"Failed to fetch hash from {link}");
|
||||
}
|
||||
|
||||
var title = dom.QuerySelector("div.postTitle h1")?.TextContent.Trim();
|
||||
if (title == null)
|
||||
{
|
||||
throw new Exception($"Failed to fetch title from {link}");
|
||||
}
|
||||
|
||||
title = StringUtil.MakeValidFileName(title, '_', false);
|
||||
|
||||
var magnet = MagnetLinkBuilder.BuildPublicMagnetLink(hash, title);
|
||||
|
||||
return await base.Download(new Uri(magnet));
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.AudioAudiobook);
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class AudioBookBayRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly NoAuthTorrentBaseSettings _settings;
|
||||
|
||||
public AudioBookBayRequestGenerator(NoAuthTorrentBaseSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
|
||||
{
|
||||
var searchUrl = _settings.BaseUrl;
|
||||
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
term = Regex.Replace(term, @"[\W]+", " ").Trim();
|
||||
|
||||
if (term.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Set("s", term);
|
||||
parameters.Set("tt", "1");
|
||||
}
|
||||
|
||||
if (parameters.Count > 0)
|
||||
{
|
||||
searchUrl += $"?{parameters.GetQueryString()}";
|
||||
}
|
||||
|
||||
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/" }.Uri.AbsoluteUri, HttpAccept.Html);
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AudioBookBayParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly NoAuthTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public AudioBookBayParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
using var doc = ParseHtmlDocument(indexerResponse.Content);
|
||||
|
||||
var rows = doc.QuerySelectorAll("div.post:has(div[class=\"postTitle\"])");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var infoUrl = _settings.BaseUrl + row.QuerySelector("div.postTitle h2 a")?.GetAttribute("href")?.Trim().TrimStart('/');
|
||||
|
||||
var title = row.QuerySelector("div.postTitle")?.TextContent.Trim();
|
||||
|
||||
var infoString = row.QuerySelector("div.postContent")?.TextContent.Trim() ?? string.Empty;
|
||||
|
||||
var matchFormat = Regex.Match(infoString, @"Format: (.+) \/", RegexOptions.IgnoreCase);
|
||||
if (matchFormat.Groups[1].Success && matchFormat.Groups[1].Value.Length > 0 && matchFormat.Groups[1].Value != "?")
|
||||
{
|
||||
title += $" [{matchFormat.Groups[1].Value.Trim()}]";
|
||||
}
|
||||
|
||||
var matchBitrate = Regex.Match(infoString, @"Bitrate: (.+)File", RegexOptions.IgnoreCase);
|
||||
if (matchBitrate.Groups[1].Success && matchBitrate.Groups[1].Value.Length > 0 && matchBitrate.Groups[1].Value != "?")
|
||||
{
|
||||
title += $" [{matchBitrate.Groups[1].Value.Trim()}]";
|
||||
}
|
||||
|
||||
var matchSize = Regex.Match(infoString, @"File Size: (.+?)s?$", RegexOptions.IgnoreCase);
|
||||
var size = matchSize.Groups[1].Success ? ParseUtil.GetBytes(matchSize.Groups[1].Value) : 0;
|
||||
|
||||
var matchDateAdded = Regex.Match(infoString, @"Posted: (\d{1,2} \D{3} \d{4})", RegexOptions.IgnoreCase);
|
||||
var publishDate = matchDateAdded.Groups[1].Success && DateTime.TryParseExact(matchDateAdded.Groups[1].Value, "d MMM yyyy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsedDate) ? parsedDate : DateTime.Now;
|
||||
|
||||
var postInfo = row.QuerySelector("div.postInfo")?.FirstChild?.TextContent.Trim().Replace("\xA0", ";") ?? string.Empty;
|
||||
var matchCategory = Regex.Match(postInfo, @"Category: (.+)$", RegexOptions.IgnoreCase);
|
||||
var genres = matchCategory.Groups[1].Success ? matchCategory.Groups[1].Value.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() : new List<string>();
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = infoUrl,
|
||||
Title = CleanTitle(title),
|
||||
Categories = new List<IndexerCategory> { NewznabStandardCategory.AudioAudiobook },
|
||||
Size = size,
|
||||
Seeders = 1,
|
||||
Peers = 1,
|
||||
PublishDate = publishDate,
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1,
|
||||
Genres = genres
|
||||
};
|
||||
|
||||
var cover = row.QuerySelector("img[src]")?.GetAttribute("src")?.Trim();
|
||||
if (!string.IsNullOrEmpty(cover))
|
||||
{
|
||||
release.PosterUrl = cover.StartsWith("http") ? cover : _settings.BaseUrl + cover;
|
||||
}
|
||||
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
private static IHtmlDocument ParseHtmlDocument(string response)
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(response);
|
||||
|
||||
var hidden = doc.QuerySelectorAll("div.post.re-ab");
|
||||
foreach (var element in hidden)
|
||||
{
|
||||
var body = doc.CreateElement("div");
|
||||
body.ClassList.Add("post");
|
||||
body.InnerHtml = Encoding.UTF8.GetString(Convert.FromBase64String(element.TextContent));
|
||||
element.Parent.ReplaceChild(body, element);
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
private static string CleanTitle(string title)
|
||||
{
|
||||
title = Regex.Replace(title, @"[\u0000-\u0008\u000A-\u001F\u0100-\uFFFF]", string.Empty, RegexOptions.Compiled);
|
||||
title = Regex.Replace(title, @"\s+", " ", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
return title.Trim();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
@@ -84,6 +84,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class AvistaZParser : AvistazParserBase
|
||||
{
|
||||
protected override string TimezoneOffset => "+01:00";
|
||||
protected override string TimezoneOffset => "+02:00";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
public class AvistazParserBase : IParseIndexerResponse
|
||||
{
|
||||
protected virtual string TimezoneOffset => "-05:00"; // Avistaz does not specify a timezone & returns server time
|
||||
protected virtual string TimezoneOffset => "-04:00"; // Avistaz does not specify a timezone & returns server time
|
||||
private readonly HashSet<string> _hdResolutions = new () { "1080p", "1080i", "720p" };
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
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;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
[Obsolete("Site has shutdown")]
|
||||
public class BB : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "BB";
|
||||
public override string[] IndexerUrls => new[] { Base64Extensions.FromBase64("aHR0cHM6Ly9iYWNvbmJpdHMub3JnLw==") };
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override string Description => "BB is a Private Torrent Tracker for 0DAY / GENERAL";
|
||||
public override string Language => "en-US";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public BB(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new BBRequestGenerator { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new BBParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
|
||||
var cookies = Cookies;
|
||||
Cookies = null;
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("keeplogged", "1")
|
||||
.AddFormParameter("login", "Log+In!")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.SetHeader("Referer", LoginUrl)
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
using var dom = parser.ParseDocument(response.Content);
|
||||
var messageEl = dom.QuerySelectorAll("#loginform");
|
||||
var messages = new List<string>();
|
||||
for (var i = 0; i < 13; i++)
|
||||
{
|
||||
var child = messageEl[0].ChildNodes[i];
|
||||
messages.Add(child.Text().Trim());
|
||||
}
|
||||
|
||||
var message = string.Join(" ", messages);
|
||||
|
||||
throw new IndexerAuthException(message);
|
||||
}
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now.AddDays(30));
|
||||
|
||||
_logger.Debug("BB authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return !httpResponse.Content.Contains("logout.php");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.AudioMP3);
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.AudioLossless);
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.PC);
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.BooksEBook);
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.AudioAudiobook);
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Other);
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.BooksMags);
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.BooksComics);
|
||||
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.TVAnime);
|
||||
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.Movies);
|
||||
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.TVHD);
|
||||
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.TVSD);
|
||||
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.TV);
|
||||
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.PCGames);
|
||||
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.Console);
|
||||
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.Other);
|
||||
caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.Other);
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class BBRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public UserPassTorrentBaseSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
{
|
||||
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
|
||||
|
||||
// TODO: IMDB search is available but it requires to parse the details page
|
||||
var qc = new NameValueCollection
|
||||
{
|
||||
{ "order_by", "s3" },
|
||||
{ "order_way", "desc" },
|
||||
{ "disablegrouping", "1" },
|
||||
{ "searchtags", "" },
|
||||
{ "tags_type", "0" },
|
||||
{ "action", "basic" },
|
||||
{ "searchstr", term.Replace(".", " ") }
|
||||
};
|
||||
|
||||
var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
|
||||
foreach (var cat in catList)
|
||||
{
|
||||
qc.Add($"filter_cat[{cat}]", "1");
|
||||
}
|
||||
|
||||
searchUrl = searchUrl + "?" + qc.GetQueryString();
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class BBParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly UserPassTorrentBaseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public BBParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = dom.QuerySelectorAll("#torrent_table > tbody > tr.torrent");
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var release = new TorrentInfo();
|
||||
|
||||
release.MinimumRatio = 1;
|
||||
release.MinimumSeedTime = 172800; // 48 hours
|
||||
|
||||
var catStr = row.Children[0].FirstElementChild.GetAttribute("href").Split(new[] { '[', ']' })[1];
|
||||
release.Categories = _categories.MapTrackerCatToNewznab(catStr);
|
||||
|
||||
var qDetails = row.Children[1].QuerySelector("a[title='View Torrent']");
|
||||
release.InfoUrl = _settings.BaseUrl + qDetails.GetAttribute("href");
|
||||
release.Guid = release.InfoUrl;
|
||||
|
||||
var qDownload = row.Children[1].QuerySelector("a[title='Download']");
|
||||
release.DownloadUrl = _settings.BaseUrl + qDownload.GetAttribute("href");
|
||||
|
||||
var dateStr = row.Children[3].TextContent.Trim().Replace(" and", "");
|
||||
release.PublishDate = DateTimeUtil.FromTimeAgo(dateStr);
|
||||
|
||||
var sizeStr = row.Children[4].TextContent;
|
||||
release.Size = ParseUtil.GetBytes(sizeStr);
|
||||
|
||||
release.Files = ParseUtil.CoerceInt(row.Children[2].TextContent.Trim());
|
||||
release.Seeders = ParseUtil.CoerceInt(row.Children[7].TextContent.Trim());
|
||||
release.Peers = ParseUtil.CoerceInt(row.Children[8].TextContent.Trim()) + release.Seeders;
|
||||
|
||||
var grabs = row.QuerySelector("td:nth-child(6)").TextContent;
|
||||
release.Grabs = ParseUtil.CoerceInt(grabs);
|
||||
|
||||
if (row.QuerySelector("strong:contains(\"Freeleech!\")") != null)
|
||||
{
|
||||
release.DownloadVolumeFactor = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
release.DownloadVolumeFactor = 1;
|
||||
}
|
||||
|
||||
release.UploadVolumeFactor = 1;
|
||||
|
||||
var title = row.QuerySelector("td:nth-child(2)");
|
||||
foreach (var element in title.QuerySelectorAll("span, strong, div, br"))
|
||||
{
|
||||
element.Remove();
|
||||
}
|
||||
|
||||
release.Title = ParseUtil.NormalizeMultiSpaces(title.TextContent.Replace(" - ]", "]"));
|
||||
|
||||
//change "Season #" to "S##" for TV shows
|
||||
if (catStr == "10")
|
||||
{
|
||||
release.Title = Regex.Replace(release.Title,
|
||||
@"Season (\d+)",
|
||||
m => string.Format("S{0:00}",
|
||||
int.Parse(m.Groups[1].Value)));
|
||||
}
|
||||
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -352,7 +352,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
[FieldDefinition(7, Label = "IndexerBeyondHDSettingsRewindOnly", Type = FieldType.Checkbox, HelpText = "IndexerBeyondHDSettingsRewindOnlyHelpText")]
|
||||
public bool RewindOnly { get; set; }
|
||||
|
||||
[FieldDefinition(8, Label = "IndexerBeyondHDSettingsSearchTypes", Type = FieldType.TagSelect, SelectOptions = typeof(BeyondHDSearchType), HelpText = "IndexerBeyondHDSettingsSearchTypesHelpText", Advanced = true)]
|
||||
[FieldDefinition(8, Label = "IndexerBeyondHDSettingsSearchTypes", Type = FieldType.Select, SelectOptions = typeof(BeyondHDSearchType), HelpText = "IndexerBeyondHDSettingsSearchTypesHelpText", Advanced = true)]
|
||||
public IEnumerable<int> SearchTypes { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -771,13 +771,14 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
|
||||
protected Dictionary<string, string> ParseCustomHeaders(Dictionary<string, List<string>> customHeaders, Dictionary<string, object> variables)
|
||||
{
|
||||
var headers = new Dictionary<string, string>();
|
||||
|
||||
if (customHeaders == null)
|
||||
{
|
||||
return null;
|
||||
return headers;
|
||||
}
|
||||
|
||||
// FIXME: fix jackett header handling (allow it to specifiy the same header multipe times)
|
||||
var headers = new Dictionary<string, string>();
|
||||
foreach (var header in customHeaders)
|
||||
{
|
||||
headers.Add(header.Key, ApplyGoTemplateText(header.Value[0], variables));
|
||||
|
||||
@@ -502,36 +502,54 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
value = release.Description;
|
||||
break;
|
||||
case "category":
|
||||
if (fieldModifiers.Contains("noappend"))
|
||||
{
|
||||
_logger.Warn("The \"noappend\" modifier is deprecated. Please switch to \"default\". See the Definition Format in the Wiki for more information.");
|
||||
}
|
||||
|
||||
var cats = _categories.MapTrackerCatToNewznab(value);
|
||||
|
||||
if (cats.Any())
|
||||
{
|
||||
if (release.Categories == null || fieldModifiers.Contains("noappend"))
|
||||
{
|
||||
release.Categories = cats;
|
||||
}
|
||||
else
|
||||
{
|
||||
release.Categories = release.Categories.Union(cats).ToList();
|
||||
}
|
||||
release.Categories = release.Categories == null || fieldModifiers.Contains("noappend")
|
||||
? cats
|
||||
: release.Categories.Union(cats).ToList();
|
||||
}
|
||||
|
||||
if (value.IsNotNullOrWhiteSpace() && !release.Categories.Any())
|
||||
{
|
||||
_logger.Warn("[{0}] Invalid category for value: '{1}'", _definition.Id, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = release.Categories.ToString();
|
||||
}
|
||||
|
||||
value = release.Categories.ToString();
|
||||
break;
|
||||
case "categorydesc":
|
||||
var catsDesc = _categories.MapTrackerCatDescToNewznab(value);
|
||||
if (catsDesc.Any())
|
||||
if (fieldModifiers.Contains("noappend"))
|
||||
{
|
||||
if (release.Categories == null || fieldModifiers.Contains("noappend"))
|
||||
{
|
||||
release.Categories = catsDesc;
|
||||
}
|
||||
else
|
||||
{
|
||||
release.Categories = release.Categories.Union(catsDesc).ToList();
|
||||
}
|
||||
_logger.Warn("The \"noappend\" modifier is deprecated. Please switch to \"default\". See the Definition Format in the Wiki for more information.");
|
||||
}
|
||||
|
||||
var catsDesc = _categories.MapTrackerCatDescToNewznab(value);
|
||||
|
||||
if (catsDesc.Any())
|
||||
{
|
||||
release.Categories = release.Categories == null || fieldModifiers.Contains("noappend")
|
||||
? catsDesc
|
||||
: release.Categories.Union(catsDesc).ToList();
|
||||
}
|
||||
|
||||
if (value.IsNotNullOrWhiteSpace() && !release.Categories.Any())
|
||||
{
|
||||
_logger.Warn("[{0}] Invalid category for value: '{1}'", _definition.Id, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = release.Categories.ToString();
|
||||
}
|
||||
|
||||
value = release.Categories.ToString();
|
||||
break;
|
||||
case "size":
|
||||
release.Size = ParseUtil.GetBytes(value);
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
protected override string TimezoneOffset => "+01:00";
|
||||
protected override string TimezoneOffset => "+02:00";
|
||||
|
||||
public ExoticaZParser(IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
|
||||
@@ -77,7 +77,7 @@ public class FileListParser : IParseIndexerResponse
|
||||
InfoUrl = GetInfoUrl(id),
|
||||
Seeders = row.Seeders,
|
||||
Peers = row.Leechers + row.Seeders,
|
||||
PublishDate = DateTime.Parse(row.UploadDate + " +0200", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||
PublishDate = DateTime.Parse(row.UploadDate + " +0300", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||
Description = row.SmallDescription,
|
||||
Genres = row.SmallDescription.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||
ImdbId = imdbId,
|
||||
|
||||
@@ -59,6 +59,12 @@ public class GazelleParser : IParseIndexerResponse
|
||||
{
|
||||
foreach (var torrent in result.Torrents)
|
||||
{
|
||||
// skip releases that cannot be used with freeleech tokens when the option is enabled
|
||||
if (Settings.UseFreeleechToken && !torrent.CanUseToken)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = torrent.TorrentId;
|
||||
|
||||
var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear}) [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
|
||||
@@ -72,14 +78,14 @@ public class GazelleParser : IParseIndexerResponse
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(id, !torrent.IsFreeLeech && !torrent.IsNeutralLeech && !torrent.IsPersonalFreeLeech),
|
||||
Title = WebUtility.HtmlDecode(title),
|
||||
Container = torrent.Encoding,
|
||||
Files = torrent.FileCount,
|
||||
Grabs = torrent.Snatches,
|
||||
Codec = torrent.Format,
|
||||
Size = long.Parse(torrent.Size),
|
||||
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken),
|
||||
InfoUrl = infoUrl,
|
||||
Seeders = int.Parse(torrent.Seeders),
|
||||
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
|
||||
PublishDate = torrent.Time.ToUniversalTime(),
|
||||
@@ -104,6 +110,12 @@ public class GazelleParser : IParseIndexerResponse
|
||||
}
|
||||
else
|
||||
{
|
||||
// skip releases that cannot be used with freeleech tokens when the option is enabled
|
||||
if (Settings.UseFreeleechToken && !result.CanUseToken)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = result.TorrentId;
|
||||
var groupName = WebUtility.HtmlDecode(result.GroupName);
|
||||
var infoUrl = GetInfoUrl(result.GroupId, id);
|
||||
@@ -111,10 +123,10 @@ public class GazelleParser : IParseIndexerResponse
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(id, !result.IsFreeLeech && !result.IsNeutralLeech && !result.IsPersonalFreeLeech),
|
||||
Title = groupName,
|
||||
Size = long.Parse(result.Size),
|
||||
DownloadUrl = GetDownloadUrl(id, result.CanUseToken),
|
||||
InfoUrl = infoUrl,
|
||||
Seeders = int.Parse(result.Seeders),
|
||||
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
|
||||
Files = result.FileCount,
|
||||
|
||||
@@ -205,7 +205,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
.AddQueryParam("request", "quick_user")
|
||||
.Build();
|
||||
|
||||
var indexResponse = await _httpClient.ExecuteAsync(request).ConfigureAwait(false);
|
||||
var indexResponse = await _httpClient.ExecuteProxiedAsync(request, Definition).ConfigureAwait(false);
|
||||
|
||||
var index = Json.Deserialize<GazelleGamesUserResponse>(indexResponse.Content);
|
||||
|
||||
@@ -412,6 +412,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
categories = _categories.MapTrackerCatToNewznab(torrent.Value.CategoryId.ToString()).ToArray();
|
||||
}
|
||||
|
||||
Enum.TryParse(torrent.Value.FreeTorrent, true, out GazelleGamesFreeTorrent freeTorrent);
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
@@ -426,8 +428,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Peers = torrent.Value.Leechers + torrent.Value.Seeders,
|
||||
PublishDate = torrent.Value.Time.ToUniversalTime(),
|
||||
Scene = torrent.Value.Scene == 1,
|
||||
DownloadVolumeFactor = torrent.Value.FreeTorrent is GazelleGamesFreeTorrent.FreeLeech or GazelleGamesFreeTorrent.Neutral || torrent.Value.LowSeedFL ? 0 : 1,
|
||||
UploadVolumeFactor = torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.Neutral ? 0 : 1,
|
||||
DownloadVolumeFactor = freeTorrent is GazelleGamesFreeTorrent.FreeLeech or GazelleGamesFreeTorrent.Neutral || torrent.Value.LowSeedFL ? 0 : 1,
|
||||
UploadVolumeFactor = freeTorrent == GazelleGamesFreeTorrent.Neutral ? 0 : 1,
|
||||
MinimumSeedTime = 288000 // Minimum of 3 days and 8 hours (80 hours in total)
|
||||
};
|
||||
|
||||
@@ -588,7 +590,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public int? Snatched { get; set; }
|
||||
public int Seeders { get; set; }
|
||||
public int Leechers { get; set; }
|
||||
public GazelleGamesFreeTorrent FreeTorrent { get; set; }
|
||||
public string FreeTorrent { get; set; }
|
||||
public bool PersonalFL { get; set; }
|
||||
public bool LowSeedFL { get; set; }
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user