mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-13 21:04:44 -04:00
Compare commits
78 Commits
cardigann-
...
ratelimit-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e15054329 | ||
|
|
3dfbfd07dd | ||
|
|
842df6913c | ||
|
|
599eeb4c61 | ||
|
|
da371dd921 | ||
|
|
fc25ba7ac0 | ||
|
|
6e1bef13e2 | ||
|
|
72ee413411 | ||
|
|
e87b45b47e | ||
|
|
cc841fe3d1 | ||
|
|
264ffdcc26 | ||
|
|
5cc044aa8f | ||
|
|
de2fd92b6f | ||
|
|
eff09c1f72 | ||
|
|
9db888c9a3 | ||
|
|
bf78396164 | ||
|
|
0e7eaa9221 | ||
|
|
5b82decc31 | ||
|
|
38ab533272 | ||
|
|
4914fcd5df | ||
|
|
858415b037 | ||
|
|
43f4899324 | ||
|
|
c60a94adfb | ||
|
|
f386ddb806 | ||
|
|
4175c2577e | ||
|
|
6ce9e5ceb9 | ||
|
|
c15643be39 | ||
|
|
a58380031d | ||
|
|
73af5c9a72 | ||
|
|
d556545e7f | ||
|
|
affde5d7b7 | ||
|
|
518c85dee2 | ||
|
|
ba3a240707 | ||
|
|
587a73f3d6 | ||
|
|
ae8f017ca8 | ||
|
|
d9098b612e | ||
|
|
29e7cc06a1 | ||
|
|
387fb0bd15 | ||
|
|
2d33560d89 | ||
|
|
94a797fc1e | ||
|
|
2e851b0588 | ||
|
|
7303cdf555 | ||
|
|
6636cbc4ae | ||
|
|
a5a4f62f25 | ||
|
|
05a7465a07 | ||
|
|
c35f1212fb | ||
|
|
ad95d73e9d | ||
|
|
30f53c20ed | ||
|
|
0199a37a0c | ||
|
|
e9764820c0 | ||
|
|
d285cbb021 | ||
|
|
8afaa3386d | ||
|
|
c94beb6814 | ||
|
|
c7eb08a0f0 | ||
|
|
2a2e859420 | ||
|
|
31f0e8212e | ||
|
|
1cbb9b1724 | ||
|
|
45dbcc6b89 | ||
|
|
3b26613394 | ||
|
|
6bb8c09fcf | ||
|
|
810b3612aa | ||
|
|
57dcd861a9 | ||
|
|
dfe132cda2 | ||
|
|
a635820b48 | ||
|
|
d959e81efb | ||
|
|
ac89cd636f | ||
|
|
50616f5c9e | ||
|
|
3f9cb2c6ea | ||
|
|
b5aa85a548 | ||
|
|
0fa5127c83 | ||
|
|
4d137886bc | ||
|
|
9dde041c99 | ||
|
|
a8234c9ce0 | ||
|
|
9227efdb65 | ||
|
|
fa923e658f | ||
|
|
364a5564ae | ||
|
|
9efd0b391e | ||
|
|
320161e051 |
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.4.11'
|
||||
majorVersion: '1.1.0'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.301'
|
||||
dotnetVersion: '6.0.404'
|
||||
innoVersion: '6.2.0'
|
||||
nodeVersion: '16.x'
|
||||
windowsImage: 'windows-2022'
|
||||
|
||||
@@ -36,7 +36,6 @@ class TagInputInput extends Component {
|
||||
<div
|
||||
ref={forwardedRef}
|
||||
className={className}
|
||||
component="div"
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
/** Outline **/
|
||||
|
||||
.outline {
|
||||
background-color: var(--white);
|
||||
background-color: var(--cardBackgroundColor);
|
||||
}
|
||||
|
||||
@@ -108,5 +108,5 @@
|
||||
/** Outline **/
|
||||
|
||||
.outline {
|
||||
background-color: var(--white);
|
||||
background-color: var(--cardBackgroundColor);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
color: var(--toobarButtonHoverColor);
|
||||
color: #515253;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||
import SignalRConnector from 'Components/SignalRConnector';
|
||||
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||
import PageHeader from './Header/PageHeader';
|
||||
import PageSidebar from './Sidebar/PageSidebar';
|
||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
||||
isSmallScreen,
|
||||
isSidebarVisible,
|
||||
enableColorImpairedMode,
|
||||
authenticationEnabled,
|
||||
onSidebarToggle,
|
||||
onSidebarVisibleChange
|
||||
} = this.props;
|
||||
@@ -109,6 +111,10 @@ class Page extends Component {
|
||||
isOpen={this.state.isConnectionLostModalOpen}
|
||||
onModalClose={this.onConnectionLostModalClose}
|
||||
/>
|
||||
|
||||
<AuthenticationRequiredModal
|
||||
isOpen={!authenticationEnabled}
|
||||
/>
|
||||
</div>
|
||||
</ColorImpairedContext.Provider>
|
||||
);
|
||||
@@ -124,6 +130,7 @@ Page.propTypes = {
|
||||
isUpdated: PropTypes.bool.isRequired,
|
||||
isDisconnected: PropTypes.bool.isRequired,
|
||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||
authenticationEnabled: PropTypes.bool.isRequired,
|
||||
onResize: PropTypes.func.isRequired,
|
||||
onSidebarToggle: PropTypes.func.isRequired,
|
||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||
|
||||
@@ -11,6 +11,7 @@ import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUI
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import ErrorPage from './ErrorPage';
|
||||
import LoadingPage from './LoadingPage';
|
||||
import Page from './Page';
|
||||
@@ -133,18 +134,21 @@ function createMapStateToProps() {
|
||||
selectErrors,
|
||||
selectAppProps,
|
||||
createDimensionsSelector(),
|
||||
createSystemStatusSelector(),
|
||||
(
|
||||
enableColorImpairedMode,
|
||||
isPopulated,
|
||||
errors,
|
||||
app,
|
||||
dimensions
|
||||
dimensions,
|
||||
systemStatus
|
||||
) => {
|
||||
return {
|
||||
...app,
|
||||
...errors,
|
||||
isPopulated,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||
enableColorImpairedMode
|
||||
};
|
||||
}
|
||||
|
||||
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModal(props) {
|
||||
const {
|
||||
isOpen
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModal;
|
||||
@@ -0,0 +1,5 @@
|
||||
.authRequiredAlert {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
165
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
165
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
@@ -0,0 +1,165 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModalContent(props) {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
settings,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
dispatchFetchStatus
|
||||
} = props;
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
const didMount = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && didMount.current) {
|
||||
dispatchFetchStatus();
|
||||
}
|
||||
|
||||
didMount.current = true;
|
||||
}, [isSaving, dispatchFetchStatus]);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
showCloseButton={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('AuthenticationRequired')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{authenticationRequiredWarning}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isPopulated && !error ?
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Authentication')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModalContent;
|
||||
@@ -0,0 +1,86 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||
|
||||
const SECTION = 'general';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(sectionSettings) => {
|
||||
return {
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchClearPendingChanges: clearPendingChanges,
|
||||
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings: saveGeneralSettings,
|
||||
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||
dispatchFetchStatus: fetchStatus
|
||||
};
|
||||
|
||||
class AuthenticationRequiredModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchGeneralSettings();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveGeneralSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchFetchGeneralSettings,
|
||||
dispatchSetGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AuthenticationRequiredModalContent
|
||||
{...otherProps}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContentConnector.propTypes = {
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
||||
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
||||
@@ -226,6 +226,42 @@ class HistoryRow extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.label ?
|
||||
<HistoryRowParameter
|
||||
title='Label'
|
||||
value={data.label}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.track ?
|
||||
<HistoryRowParameter
|
||||
title='Track'
|
||||
value={data.track}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.year ?
|
||||
<HistoryRowParameter
|
||||
title='Year'
|
||||
value={data.year}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.genre ?
|
||||
<HistoryRowParameter
|
||||
title='Genre'
|
||||
value={data.genre}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.author ?
|
||||
<HistoryRowParameter
|
||||
@@ -243,6 +279,15 @@ class HistoryRow extends Component {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.publisher ?
|
||||
<HistoryRowParameter
|
||||
title='Publisher'
|
||||
value={data.publisher}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class AddIndexerModalContent extends Component {
|
||||
const filteredIndexers = indexers.filter((indexer) => {
|
||||
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
||||
|
||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@ class IndexerIndex extends Component {
|
||||
saveError,
|
||||
isDeleting,
|
||||
isTestingAll,
|
||||
isSyncingIndexers,
|
||||
deleteError,
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
@@ -309,6 +310,15 @@ class IndexerIndex extends Component {
|
||||
onPress={this.onAddIndexerPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('SyncAppIndexers')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isSyncingIndexers}
|
||||
onPress={this.props.onAppIndexerSyncPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('TestAllIndexers')}
|
||||
iconName={icons.TEST}
|
||||
@@ -493,10 +503,12 @@ IndexerIndex.propTypes = {
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
isTestingAll: PropTypes.bool.isRequired,
|
||||
isSyncingIndexers: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onTestAllPress: PropTypes.func.isRequired,
|
||||
onAppIndexerSyncPress: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -2,10 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { testAllIndexers } from 'Store/Actions/indexerActions';
|
||||
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
|
||||
import IndexerIndex from './IndexerIndex';
|
||||
@@ -13,13 +16,16 @@ import IndexerIndex from './IndexerIndex';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
|
||||
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
indexers,
|
||||
isSyncingIndexers,
|
||||
dimensionsState
|
||||
) => {
|
||||
return {
|
||||
...indexers,
|
||||
isSyncingIndexers,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
}
|
||||
@@ -46,6 +52,12 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
|
||||
onTestAllPress() {
|
||||
dispatch(testAllIndexers());
|
||||
},
|
||||
|
||||
onAppIndexerSyncPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.APP_INDEXER_SYNC
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ function CapabilitiesLabel(props) {
|
||||
let filteredList = categories.filter((item) => item.id < 100000);
|
||||
|
||||
if (categoryFilter.length > 0) {
|
||||
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id));
|
||||
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
|
||||
}
|
||||
|
||||
const nameList = filteredList.map((item) => item.name).sort();
|
||||
|
||||
49
frontend/src/Search/Mobile/SearchIndexOverview.css
Normal file
49
frontend/src/Search/Mobile/SearchIndexOverview.css
Normal file
@@ -0,0 +1,49 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.container {
|
||||
border-radius: 4px;
|
||||
background-color: var(--cardBackgroundColor);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 10px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.indexerRow {
|
||||
color: var(--disabledColor);
|
||||
}
|
||||
|
||||
.infoRow {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
width: 85%;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
202
frontend/src/Search/Mobile/SearchIndexOverview.js
Normal file
202
frontend/src/Search/Mobile/SearchIndexOverview.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import CategoryLabel from 'Search/Table/CategoryLabel';
|
||||
import Peers from 'Search/Table/Peers';
|
||||
import ProtocolLabel from 'Search/Table/ProtocolLabel';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SearchIndexOverview.css';
|
||||
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||
|
||||
function getContentHeight(rowHeight, isSmallScreen) {
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
|
||||
return rowHeight - padding;
|
||||
}
|
||||
|
||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
return icons.DOWNLOADING;
|
||||
} else if (grabError) {
|
||||
return icons.DOWNLOADING;
|
||||
}
|
||||
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return translate('AddedToDownloadClient');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return translate('AddToDownloadClient');
|
||||
}
|
||||
|
||||
class SearchIndexOverview extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onGrabPress = () => {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
infoUrl,
|
||||
protocol,
|
||||
downloadUrl,
|
||||
categories,
|
||||
seeders,
|
||||
leechers,
|
||||
size,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
indexer,
|
||||
rowHeight,
|
||||
isSmallScreen,
|
||||
isGrabbed,
|
||||
isGrabbing,
|
||||
grabError
|
||||
} = this.props;
|
||||
|
||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info} style={{ height: contentHeight }}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.title}>
|
||||
<Link
|
||||
to={infoUrl}
|
||||
title={title}
|
||||
>
|
||||
<TextTruncate
|
||||
line={2}
|
||||
text={title}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={this.onGrabPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.downloadLink}
|
||||
name={icons.SAVE}
|
||||
title={translate('Save')}
|
||||
to={downloadUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.indexerRow}>
|
||||
{indexer}
|
||||
</div>
|
||||
<div className={styles.infoRow}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
|
||||
{
|
||||
protocol === 'torrent' &&
|
||||
<Peers
|
||||
seeders={seeders}
|
||||
leechers={leechers}
|
||||
/>
|
||||
}
|
||||
|
||||
<Label>
|
||||
{formatBytes(size)}
|
||||
</Label>
|
||||
|
||||
<Label>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</Label>
|
||||
|
||||
<CategoryLabel
|
||||
categories={categories}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchIndexOverview.propTypes = {
|
||||
guid: PropTypes.string.isRequired,
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
age: PropTypes.number.isRequired,
|
||||
ageHours: PropTypes.number.isRequired,
|
||||
ageMinutes: PropTypes.number.isRequired,
|
||||
publishDate: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
infoUrl: PropTypes.string.isRequired,
|
||||
downloadUrl: PropTypes.string.isRequired,
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
indexer: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
files: PropTypes.number,
|
||||
grabs: PropTypes.number,
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number,
|
||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
rowHeight: PropTypes.number.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isGrabbed: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.string
|
||||
};
|
||||
|
||||
SearchIndexOverview.defaultProps = {
|
||||
isGrabbing: false,
|
||||
isGrabbed: false
|
||||
};
|
||||
|
||||
export default SearchIndexOverview;
|
||||
11
frontend/src/Search/Mobile/SearchIndexOverviews.css
Normal file
11
frontend/src/Search/Mobile/SearchIndexOverviews.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.grid {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
&:hover {
|
||||
.content {
|
||||
background-color: var(--tableRowHoverBackgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
209
frontend/src/Search/Mobile/SearchIndexOverviews.js
Normal file
209
frontend/src/Search/Mobile/SearchIndexOverviews.js
Normal file
@@ -0,0 +1,209 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import Measure from 'Components/Measure';
|
||||
import SearchIndexItemConnector from 'Search/Table/SearchIndexItemConnector';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import SearchIndexOverview from './SearchIndexOverview';
|
||||
import styles from './SearchIndexOverviews.css';
|
||||
|
||||
class SearchIndexOverviews extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0,
|
||||
columnCount: 1,
|
||||
rowHeight: 100,
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
jumpToCharacter,
|
||||
scrollTop,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
rowHeight,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.sortKey !== sortKey) {
|
||||
this.calculateGrid(this.state.width, isSmallScreen);
|
||||
}
|
||||
|
||||
if (
|
||||
this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items, 'guid')
|
||||
)
|
||||
) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (this._grid && index != null) {
|
||||
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: index,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
};
|
||||
|
||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||
const rowHeight = 100;
|
||||
|
||||
this.setState({
|
||||
width,
|
||||
rowHeight
|
||||
});
|
||||
};
|
||||
|
||||
cellRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
isSmallScreen,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
const release = items[rowIndex];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<SearchIndexItemConnector
|
||||
key={release.guid}
|
||||
component={SearchIndexOverview}
|
||||
rowHeight={rowHeight}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
isSmallScreen={isSmallScreen}
|
||||
style={style}
|
||||
guid={release.guid}
|
||||
onGrabPress={onGrabPress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.calculateGrid(width, this.props.isSmallScreen);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<WindowScroller
|
||||
scrollElement={undefined}
|
||||
>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
ref={this.setGridRef}
|
||||
className={styles.grid}
|
||||
autoHeight={true}
|
||||
height={height}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
rowCount={items.length}
|
||||
rowHeight={rowHeight}
|
||||
width={width}
|
||||
onScroll={onChildScroll}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
scrollToAlignment={'start'}
|
||||
isScrollingOptOut={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchIndexOverviews.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SearchIndexOverviews;
|
||||
32
frontend/src/Search/Mobile/SearchIndexOverviewsConnector.js
Normal file
32
frontend/src/Search/Mobile/SearchIndexOverviewsConnector.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { grabRelease } from 'Store/Actions/releaseActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import SearchIndexOverviews from './SearchIndexOverviews';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(uiSettings, dimensions) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onGrabPress(payload) {
|
||||
dispatch(grabRelease(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(SearchIndexOverviews);
|
||||
@@ -11,8 +11,6 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import NoIndexer from 'Indexer/NoIndexer';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
@@ -23,12 +21,17 @@ import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
|
||||
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
|
||||
import SearchIndexOverviewsConnector from './Mobile/SearchIndexOverviewsConnector';
|
||||
import NoSearchResults from './NoSearchResults';
|
||||
import SearchFooterConnector from './SearchFooterConnector';
|
||||
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
|
||||
import styles from './SearchIndex.css';
|
||||
|
||||
function getViewComponent() {
|
||||
function getViewComponent(isSmallScreen) {
|
||||
if (isSmallScreen) {
|
||||
return SearchIndexOverviewsConnector;
|
||||
}
|
||||
|
||||
return SearchIndexTableConnector;
|
||||
}
|
||||
|
||||
@@ -44,8 +47,6 @@ class SearchIndex extends Component {
|
||||
scroller: null,
|
||||
jumpBarItems: { order: [] },
|
||||
jumpToCharacter: null,
|
||||
isAddIndexerModalOpen: false,
|
||||
isEditIndexerModalOpen: false,
|
||||
searchType: null,
|
||||
lastToggled: null,
|
||||
allSelected: false,
|
||||
@@ -177,21 +178,6 @@ class SearchIndex extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddIndexerPress = () => {
|
||||
this.setState({ isAddIndexerModalOpen: true });
|
||||
};
|
||||
|
||||
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
|
||||
this.setState({
|
||||
isAddIndexerModalOpen: false,
|
||||
isEditIndexerModalOpen: indexerSelected
|
||||
});
|
||||
};
|
||||
|
||||
onEditIndexerModalClose = () => {
|
||||
this.setState({ isEditIndexerModalOpen: false });
|
||||
};
|
||||
|
||||
onJumpBarItemPress = (jumpToCharacter) => {
|
||||
this.setState({ jumpToCharacter });
|
||||
};
|
||||
@@ -253,6 +239,7 @@ class SearchIndex extends Component {
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
onFilterSelect,
|
||||
isSmallScreen,
|
||||
hasIndexers,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -260,8 +247,6 @@ class SearchIndex extends Component {
|
||||
const {
|
||||
scroller,
|
||||
jumpBarItems,
|
||||
isAddIndexerModalOpen,
|
||||
isEditIndexerModalOpen,
|
||||
jumpToCharacter,
|
||||
selectedState,
|
||||
allSelected,
|
||||
@@ -270,7 +255,7 @@ class SearchIndex extends Component {
|
||||
|
||||
const selectedIndexerIds = this.getSelectedIds();
|
||||
|
||||
const ViewComponent = getViewComponent();
|
||||
const ViewComponent = getViewComponent(isSmallScreen);
|
||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||
const hasNoIndexer = !totalItems;
|
||||
|
||||
@@ -384,16 +369,6 @@ class SearchIndex extends Component {
|
||||
onSearchPress={this.onSearchPress}
|
||||
onBulkGrabPress={this.onBulkGrabPress}
|
||||
/>
|
||||
|
||||
<AddIndexerModal
|
||||
isOpen={isAddIndexerModalOpen}
|
||||
onModalClose={this.onAddIndexerModalClose}
|
||||
/>
|
||||
|
||||
<EditIndexerModalConnector
|
||||
isOpen={isEditIndexerModalOpen}
|
||||
onModalClose={this.onEditIndexerModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
.category {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 110px;
|
||||
flex: 0 0 130px;
|
||||
}
|
||||
|
||||
.age,
|
||||
|
||||
@@ -18,20 +18,20 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createReleaseSelector(),
|
||||
(
|
||||
movie
|
||||
release
|
||||
) => {
|
||||
|
||||
// If a movie is deleted this selector may fire before the parent
|
||||
// selecors, which will result in an undefined movie, if that happens
|
||||
// If a release is deleted this selector may fire before the parent
|
||||
// selecors, which will result in an undefined release, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
// trying to show a movie that has no information available.
|
||||
// trying to show a release that has no information available.
|
||||
|
||||
if (!movie) {
|
||||
if (!release) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
...movie
|
||||
...release
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -41,7 +41,7 @@ const mapDispatchToProps = {
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class MovieIndexItemConnector extends Component {
|
||||
class SearchIndexItemConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -66,9 +66,9 @@ class MovieIndexItemConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
MovieIndexItemConnector.propTypes = {
|
||||
SearchIndexItemConnector.propTypes = {
|
||||
guid: PropTypes.string,
|
||||
component: PropTypes.elementType.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
.category {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 110px;
|
||||
flex: 0 0 130px;
|
||||
}
|
||||
|
||||
.age,
|
||||
|
||||
@@ -81,8 +81,6 @@ class EditDownloadClientModalContent extends Component {
|
||||
message
|
||||
} = item;
|
||||
|
||||
console.log(supportsCategories);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
|
||||
@@ -11,12 +11,20 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const authenticationMethodOptions = [
|
||||
{ key: 'none', value: 'None' },
|
||||
export const authenticationRequiredWarning = translate('AuthenticationRequiredWarning');
|
||||
|
||||
export const authenticationMethodOptions = [
|
||||
{ key: 'none', value: 'None', isDisabled: true },
|
||||
{ key: 'external', value: 'External', isHidden: true },
|
||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||
];
|
||||
|
||||
export const authenticationRequiredOptions = [
|
||||
{ key: 'enabled', value: 'Enabled' },
|
||||
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
|
||||
];
|
||||
|
||||
const certificateValidationOptions = [
|
||||
{ key: 'enabled', value: 'Enabled' },
|
||||
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
||||
@@ -68,6 +76,7 @@ class SecuritySettings extends Component {
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
apiKey,
|
||||
@@ -86,13 +95,31 @@ class SecuritySettings extends Component {
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={authenticationRequiredWarning}
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
@@ -102,11 +129,12 @@ class SecuritySettings extends Component {
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
@@ -116,7 +144,8 @@ class SecuritySettings extends Component {
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
|
||||
@@ -14,8 +14,6 @@ function createLanguagesSelector() {
|
||||
return createSelector(
|
||||
(state) => state.localization,
|
||||
(localization) => {
|
||||
console.log(localization);
|
||||
|
||||
const items = localization.items;
|
||||
|
||||
if (!items) {
|
||||
|
||||
@@ -113,8 +113,6 @@ export default {
|
||||
|
||||
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
|
||||
|
||||
console.log(saveData);
|
||||
|
||||
// we have to set id since not actually posting to server yet
|
||||
if (!saveData.id) {
|
||||
saveData.id = getNextId(getState().settings.downloadClientCategories.items);
|
||||
|
||||
@@ -310,8 +310,6 @@ export const actionHandlers = handleThunks({
|
||||
isGrabbing: true
|
||||
}));
|
||||
|
||||
console.log(payload);
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/search/bulk',
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import filesize from 'filesize';
|
||||
import { filesize } from 'filesize';
|
||||
|
||||
function formatBytes(input) {
|
||||
const size = Number(input);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
|
||||
<meta name="description" content="Prowlarr (Preview)" />
|
||||
<meta name="description" content="Prowlarr" />
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
@@ -50,7 +50,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
||||
<!-- webpack bundles head -->
|
||||
|
||||
<title>Prowlarr (Preview)</title>
|
||||
<title>Prowlarr</title>
|
||||
|
||||
<!--
|
||||
The super basic styling for .root will live here,
|
||||
|
||||
107
package.json
107
package.json
@@ -25,107 +25,108 @@
|
||||
"not chrome < 60"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.1.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.18",
|
||||
"@microsoft/signalr": "6.0.6",
|
||||
"@sentry/browser": "6.19.2",
|
||||
"@sentry/integrations": "6.19.2",
|
||||
"chart.js": "3.7.1",
|
||||
"classnames": "2.3.1",
|
||||
"clipboard": "2.0.10",
|
||||
"connected-react-router": "6.9.1",
|
||||
"@fortawesome/fontawesome-free": "6.2.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@microsoft/signalr": "6.0.11",
|
||||
"@sentry/browser": "7.28.0",
|
||||
"@sentry/integrations": "7.28.0",
|
||||
"chart.js": "4.1.1",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
"connected-react-router": "6.9.3",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "6.3.0",
|
||||
"filesize": "10.0.6",
|
||||
"history": "4.10.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.6.0",
|
||||
"jquery": "3.6.2",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.2",
|
||||
"moment": "2.29.4",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.10.3",
|
||||
"qs": "6.11.0",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
"react-autosuggest": "10.1.0",
|
||||
"react-custom-scrollbars-2": "4.4.0",
|
||||
"react-custom-scrollbars-2": "4.5.0",
|
||||
"react-dnd": "14.0.4",
|
||||
"react-dnd-html5-backend": "14.0.2",
|
||||
"react-dnd-multi-backend": "6.0.2",
|
||||
"react-dnd-touch-backend": "14.1.1",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "2.5.0",
|
||||
"react-focus-lock": "2.9.2",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-measure": "1.4.7",
|
||||
"react-popper": "1.3.7",
|
||||
"react-redux": "7.2.4",
|
||||
"react-redux": "8.0.5",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-virtualized": "9.21.1",
|
||||
"redux": "4.1.0",
|
||||
"redux": "4.2.0",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.3.0",
|
||||
"reselect": "4.0.0"
|
||||
"redux-thunk": "2.4.2",
|
||||
"reselect": "4.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.18.2",
|
||||
"@babel/eslint-parser": "7.18.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.17.12",
|
||||
"@babel/plugin-proposal-decorators": "7.18.2",
|
||||
"@babel/plugin-proposal-export-default-from": "7.17.12",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.17.12",
|
||||
"@babel/plugin-proposal-function-sent": "7.18.2",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.17.12",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.17.12",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.16.7",
|
||||
"@babel/core": "7.20.5",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "7.20.5",
|
||||
"@babel/plugin-proposal-export-default-from": "7.18.10",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
|
||||
"@babel/plugin-proposal-function-sent": "7.18.6",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.18.9",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.18.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.18.2",
|
||||
"@babel/preset-react": "7.17.12",
|
||||
"autoprefixer": "10.4.7",
|
||||
"babel-loader": "8.2.5",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"autoprefixer": "10.4.13",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.22.8",
|
||||
"css-loader": "6.7.1",
|
||||
"eslint": "8.17.0",
|
||||
"core-js": "3.26.1",
|
||||
"css-loader": "6.7.3",
|
||||
"eslint": "8.30.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-react": "7.30.0",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-plugin-simple-import-sort": "8.0.0",
|
||||
"esprint": "3.6.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "6.1.7",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"loader-utils": "^3.0.0",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"postcss": "8.4.14",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.2",
|
||||
"postcss": "8.4.20",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-mixins": "9.0.2",
|
||||
"postcss-nested": "5.0.6",
|
||||
"postcss-simple-vars": "6.0.3",
|
||||
"postcss-loader": "7.0.2",
|
||||
"postcss-mixins": "9.0.4",
|
||||
"postcss-nested": "6.0.0",
|
||||
"postcss-simple-vars": "7.0.1",
|
||||
"postcss-url": "10.1.3",
|
||||
"require-nocache": "1.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "14.8.5",
|
||||
"stylelint": "14.16.0",
|
||||
"stylelint-order": "5.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.73.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack": "5.75.0",
|
||||
"webpack-cli": "5.0.1",
|
||||
"webpack-livereload-plugin": "3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test
|
||||
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||
_runner.KillAll();
|
||||
_runner.Start();
|
||||
_runner.Start(true);
|
||||
|
||||
driver.Url = "http://localhost:9696";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Common.Http
|
||||
{
|
||||
@@ -25,5 +25,11 @@ namespace NzbDrone.Common.Http
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TooManyRequestsException(HttpRequest request, HttpResponse response, TimeSpan retryWait)
|
||||
: base(request, response)
|
||||
{
|
||||
RetryAfter = retryWait;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,26 +11,41 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
try
|
||||
{
|
||||
sentryEvent.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
|
||||
if (sentryEvent.Message is not null)
|
||||
{
|
||||
sentryEvent.Message.Formatted = CleanseLogMessage.Cleanse(sentryEvent.Message.Formatted);
|
||||
sentryEvent.Message.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
|
||||
sentryEvent.Message.Params = sentryEvent.Message.Params?.Select(x => CleanseLogMessage.Cleanse(x switch
|
||||
{
|
||||
string str => str,
|
||||
_ => x.ToString()
|
||||
})).ToList();
|
||||
}
|
||||
|
||||
if (sentryEvent.Fingerprint != null)
|
||||
if (sentryEvent.Fingerprint.Any())
|
||||
{
|
||||
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
|
||||
sentryEvent.SetFingerprint(fingerprint);
|
||||
}
|
||||
|
||||
if (sentryEvent.Extra != null)
|
||||
if (sentryEvent.Extra.Any())
|
||||
{
|
||||
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse((string)y.Value));
|
||||
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse(y.Value as string));
|
||||
sentryEvent.SetExtras(extras);
|
||||
}
|
||||
|
||||
foreach (var exception in sentryEvent.SentryExceptions)
|
||||
if (sentryEvent.SentryExceptions is not null)
|
||||
{
|
||||
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
||||
foreach (var frame in exception.Stacktrace.Frames)
|
||||
foreach (var exception in sentryEvent.SentryExceptions)
|
||||
{
|
||||
frame.FileName = ShortenPath(frame.FileName);
|
||||
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
||||
if (exception.Stacktrace is not null)
|
||||
{
|
||||
foreach (var frame in exception.Stacktrace.Frames)
|
||||
{
|
||||
frame.FileName = ShortenPath(frame.FileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
"UnauthorizedAccessException",
|
||||
|
||||
// Filter out people stuck in boot loops
|
||||
"CorruptDatabaseException",
|
||||
|
||||
// This also filters some people in boot loops
|
||||
"TinyIoCResolutionException"
|
||||
"CorruptDatabaseException"
|
||||
};
|
||||
|
||||
public static readonly List<string> FilteredExceptionMessages = new List<string>
|
||||
@@ -102,9 +99,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
o.Dsn = dsn;
|
||||
o.AttachStacktrace = true;
|
||||
o.MaxBreadcrumbs = 200;
|
||||
o.SendDefaultPii = false;
|
||||
o.Debug = false;
|
||||
o.DiagnosticLevel = SentryLevel.Debug;
|
||||
o.Release = BuildInfo.Release;
|
||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||
@@ -210,7 +204,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
if (ex != null)
|
||||
{
|
||||
fingerPrint.Add(ex.GetType().FullName);
|
||||
fingerPrint.Add(ex.TargetSite.ToString());
|
||||
if (ex.TargetSite != null)
|
||||
{
|
||||
fingerPrint.Add(ex.TargetSite.ToString());
|
||||
}
|
||||
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NLog" Version="5.0.1" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
||||
<PackageReference Include="Sentry" Version="3.21.0" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="NLog" Version="5.1.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
|
||||
<PackageReference Include="Sentry" Version="3.24.1" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<subcat id="5030" name="SD"/>
|
||||
<subcat id="5060" name="Sport"/>
|
||||
<subcat id="5010" name="WEB-DL"/>
|
||||
<subcat id="5999" name="Other"/>
|
||||
</category>
|
||||
<category id="7000" name="Other">
|
||||
<subcat id="7010" name="Misc"/>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"status": "success",
|
||||
"response": {
|
||||
"currentPage": 1,
|
||||
"pages": 1,
|
||||
"results": [
|
||||
{
|
||||
"groupId": 2497,
|
||||
"groupName": "Singin' in the Rain",
|
||||
"artist": "Gene Kelly & Stanley Donen",
|
||||
"cover": "https:\/\/www.themoviedb.org\/t\/p\/original\/g2AaJDC2vSRcqHSDH29642xmQd.jpg",
|
||||
"tags": [ "comedy", "musical", "romance" ],
|
||||
"bookmarked": false,
|
||||
"vanityHouse": false,
|
||||
"groupYear": 1952,
|
||||
"releaseType": null,
|
||||
"groupTime": "1671129449",
|
||||
"maxSize": 57473058680,
|
||||
"totalSnatched": 25,
|
||||
"totalSeeders": 9,
|
||||
"totalLeechers": 0,
|
||||
"torrents": [
|
||||
{
|
||||
"torrentId": 3599,
|
||||
"editionId": 1,
|
||||
"artists": [
|
||||
{
|
||||
"id": 126,
|
||||
"name": "Gene Kelly",
|
||||
"aliasid": 127
|
||||
},
|
||||
{
|
||||
"id": 125,
|
||||
"name": "Stanley Donen",
|
||||
"aliasid": 126
|
||||
}
|
||||
],
|
||||
"remastered": false,
|
||||
"remasterYear": 0,
|
||||
"remasterCatalogueNumber": "",
|
||||
"remasterTitle": "",
|
||||
"media": "1080p",
|
||||
"encoding": "",
|
||||
"format": "",
|
||||
"hasLog": false,
|
||||
"logScore": 0,
|
||||
"hasCue": false,
|
||||
"scene": false,
|
||||
"vanityHouse": false,
|
||||
"fileCount": 1,
|
||||
"time": "2017-09-10 11:47:27",
|
||||
"size": 24724893991,
|
||||
"snatches": 14,
|
||||
"seeders": 1,
|
||||
"leechers": 0,
|
||||
"isFreeleech": true,
|
||||
"isNeutralLeech": false,
|
||||
"isPersonalFreeleech": false,
|
||||
"canUseToken": false,
|
||||
"hasSnatched": false
|
||||
},
|
||||
{
|
||||
"torrentId": 45068,
|
||||
"editionId": 2,
|
||||
"artists": [
|
||||
{
|
||||
"id": 126,
|
||||
"name": "Gene Kelly",
|
||||
"aliasid": 127
|
||||
},
|
||||
{
|
||||
"id": 125,
|
||||
"name": "Stanley Donen",
|
||||
"aliasid": 126
|
||||
}
|
||||
],
|
||||
"remastered": false,
|
||||
"remasterYear": 0,
|
||||
"remasterCatalogueNumber": "",
|
||||
"remasterTitle": "",
|
||||
"media": "2160p",
|
||||
"encoding": "",
|
||||
"format": "",
|
||||
"hasLog": false,
|
||||
"logScore": 0,
|
||||
"hasCue": false,
|
||||
"scene": false,
|
||||
"vanityHouse": false,
|
||||
"fileCount": 1,
|
||||
"time": "2022-12-15 19:37:29",
|
||||
"size": 57473058680,
|
||||
"snatches": 6,
|
||||
"seeders": 8,
|
||||
"leechers": 0,
|
||||
"isFreeleech": true,
|
||||
"isNeutralLeech": false,
|
||||
"isPersonalFreeleech": false,
|
||||
"canUseToken": false,
|
||||
"hasSnatched": false
|
||||
},
|
||||
{
|
||||
"torrentId": 2726,
|
||||
"editionId": 3,
|
||||
"artists": [
|
||||
{
|
||||
"id": 126,
|
||||
"name": "Gene Kelly",
|
||||
"aliasid": 127
|
||||
},
|
||||
{
|
||||
"id": 125,
|
||||
"name": "Stanley Donen",
|
||||
"aliasid": 126
|
||||
}
|
||||
],
|
||||
"remastered": false,
|
||||
"remasterYear": 0,
|
||||
"remasterCatalogueNumber": "",
|
||||
"remasterTitle": "",
|
||||
"media": "DVD-R",
|
||||
"encoding": "",
|
||||
"format": "",
|
||||
"hasLog": false,
|
||||
"logScore": 0,
|
||||
"hasCue": false,
|
||||
"scene": false,
|
||||
"vanityHouse": false,
|
||||
"fileCount": 37,
|
||||
"time": "2017-08-26 14:58:58",
|
||||
"size": 10350032896,
|
||||
"snatches": 5,
|
||||
"seeders": 0,
|
||||
"leechers": 0,
|
||||
"isFreeleech": true,
|
||||
"isNeutralLeech": false,
|
||||
"isPersonalFreeleech": false,
|
||||
"canUseToken": false,
|
||||
"hasSnatched": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<caps>
|
||||
<server version="1.0" title="Anime Tosho" strapline="Anime NZB/DDL mirror" url="https://animetosho.org/"/>
|
||||
<limits max="200" default="75"/>
|
||||
<retention days="9999"/>
|
||||
<registration available="no" open="yes" />
|
||||
<searching>
|
||||
<search available="yes" supportedParams="q" />
|
||||
<tv-search available="no" supportedParams="q" />
|
||||
<movie-search available="no" supportedParams="q" />
|
||||
</searching>
|
||||
<categories>
|
||||
<category id="5070" name="Anime" description="Anime"/>
|
||||
</categories>
|
||||
</caps>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:torznab="http://torznab.com/schemas/2015/feed">
|
||||
<channel>
|
||||
<item>
|
||||
<title>Out of the Past 1947 720p BluRay FLAC2.0 x264-CtrlHD.mkv</title>
|
||||
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164</guid>
|
||||
<link>https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)</link>
|
||||
<comments>https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164</comments>
|
||||
<pubDate>Tue, 20 Dec 2022 21:32:17 +0000</pubDate>
|
||||
<size>5412993028</size>
|
||||
<files>1</files>
|
||||
<grabs>2</grabs>
|
||||
<category>2000</category>
|
||||
<category>2040</category>
|
||||
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
|
||||
<enclosure url="https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)" length="103641" type="application/x-bittorrent" />
|
||||
<torznab:attr name="size" value="5412993028" />
|
||||
<torznab:attr name="poster" value="anon" />
|
||||
<torznab:attr name="seeders" value="3" />
|
||||
<torznab:attr name="leechers" value="0" />
|
||||
<torznab:attr name="peers" value="3" />
|
||||
<torznab:attr name="infohash" value="(removed)" />
|
||||
<torznab:attr name="downloadvolumefactor" value="1" />
|
||||
<torznab:attr name="uploadvolumefactor" value="1" />
|
||||
<torznab:attr name="tag" value="anonymous" />
|
||||
<torznab:attr name="imdb" value="0039689" />
|
||||
<torznab:attr name="imdbid" value="tt0039689" />
|
||||
</item>
|
||||
<item>
|
||||
<title>Out of the Past 1947 1080p USA Blu-ray AVC DTS-HD MA 2.0-PCH</title>
|
||||
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836165</guid>
|
||||
<link>https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)</link>
|
||||
<comments>https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836165</comments>
|
||||
<pubDate>Tue, 20 Dec 2022 21:47:40 +0000</pubDate>
|
||||
<size>30524085127</size>
|
||||
<files>78</files>
|
||||
<grabs>0</grabs>
|
||||
<category>2000</category>
|
||||
<category>2040</category>
|
||||
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
|
||||
<enclosure url="https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)" length="150224" type="application/x-bittorrent" />
|
||||
<torznab:attr name="size" value="30524085127" />
|
||||
<torznab:attr name="poster" value="anon" />
|
||||
<torznab:attr name="seeders" value="1" />
|
||||
<torznab:attr name="leechers" value="0" />
|
||||
<torznab:attr name="peers" value="1" />
|
||||
<torznab:attr name="infohash" value="(removed)" />
|
||||
<torznab:attr name="downloadvolumefactor" value="1" />
|
||||
<torznab:attr name="uploadvolumefactor" value="1" />
|
||||
<torznab:attr name="tag" value="anonymous" />
|
||||
<torznab:attr name="imdb" value="0039689" />
|
||||
<torznab:attr name="imdbid" value="tt0039689" />
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -28,15 +28,15 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[Test]
|
||||
public void should_return_warning_when_branch_not_valid()
|
||||
{
|
||||
GivenValidBranch("master");
|
||||
GivenValidBranch("test");
|
||||
|
||||
Subject.Check().ShouldBeWarning();
|
||||
}
|
||||
|
||||
[TestCase("Develop")]
|
||||
[TestCase("develop")]
|
||||
[TestCase("nightly")]
|
||||
[TestCase("Nightly")]
|
||||
[TestCase("develop")]
|
||||
[TestCase("master")]
|
||||
public void should_return_no_warning_when_branch_valid(string branch)
|
||||
{
|
||||
GivenValidBranch(branch);
|
||||
|
||||
@@ -71,12 +71,12 @@ namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{{ .Today.Year }}", "2022")]
|
||||
public void should_handle_variables_statements(string template, string expected)
|
||||
[TestCase("{{ .Today.Year }}")]
|
||||
public void should_handle_variables_statements(string template)
|
||||
{
|
||||
var result = Subject.ApplyGoTemplateText(template, _variables);
|
||||
|
||||
result.Should().Be(expected);
|
||||
result.Should().Be(DateTime.Now.Year.ToString());
|
||||
}
|
||||
|
||||
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
|
||||
|
||||
@@ -50,7 +50,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 22:20:19"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19").ToUniversalTime());
|
||||
torrentInfo.Size.Should().Be(8300512414);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
@@ -15,14 +16,14 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[TestFixture]
|
||||
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
|
||||
{
|
||||
private GenericNewznabSettings _settings;
|
||||
private NewznabSettings _settings;
|
||||
private IndexerDefinition _definition;
|
||||
private string _caps;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_settings = new GenericNewznabSettings()
|
||||
_settings = new NewznabSettings()
|
||||
{
|
||||
BaseUrl = "http://indxer.local"
|
||||
};
|
||||
@@ -70,6 +71,45 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
caps.LimitsMax.Value.Should().Be(60);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_map_different_categories()
|
||||
{
|
||||
GivenCapsResponse(_caps);
|
||||
|
||||
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||
|
||||
var bookCats = caps.Categories.MapTorznabCapsToTrackers(new int[] { NewznabStandardCategory.Books.Id });
|
||||
|
||||
bookCats.Count.Should().Be(2);
|
||||
bookCats.Should().Contain("8000");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_find_sub_categories_as_main_categories()
|
||||
{
|
||||
GivenCapsResponse(ReadAllText("Files/Indexers/Torznab/torznab_animetosho_caps.xml"));
|
||||
|
||||
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||
|
||||
var bookCats = caps.Categories.MapTrackerCatToNewznab("5070");
|
||||
|
||||
bookCats.Count.Should().Be(2);
|
||||
bookCats.First().Id.Should().Be(5070);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_map_by_name_when_available()
|
||||
{
|
||||
GivenCapsResponse(_caps);
|
||||
|
||||
var caps = Subject.GetCapabilities(_settings, _definition);
|
||||
|
||||
var bookCats = caps.Categories.MapTrackerCatToNewznab("5999");
|
||||
|
||||
bookCats.Count.Should().Be(2);
|
||||
bookCats.First().Id.Should().Be(5050);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_default_pagesize_if_missing()
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
|
||||
_caps = new IndexerCapabilities();
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_caps);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
public class NewznabRequestGeneratorFixture : CoreTest<GenericNewznabRequestGenerator>
|
||||
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
|
||||
{
|
||||
private MovieSearchCriteria _movieSearchCriteria;
|
||||
private TvSearchCriteria _tvSearchCriteria;
|
||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
Subject.Settings = new GenericNewznabSettings()
|
||||
Subject.Settings = new NewznabSettings()
|
||||
{
|
||||
BaseUrl = "http://127.0.0.1:1234/",
|
||||
ApiKey = "abcd",
|
||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_capabilities = new IndexerCapabilities();
|
||||
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_capabilities);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_parse_recent_feed_from_GazelleGames()
|
||||
public async Task should_parse_recent_feed_from_Orpheus()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Orpheus/recentfeed.json");
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Definitions;
|
||||
using NzbDrone.Core.Indexers.Gazelle;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SecretCinemaFixture : CoreTest<SecretCinema>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "SecretCinema",
|
||||
Settings = new GazelleSettings() { Username = "somekey", Password = "somekey" }
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_parse_recent_feed_from_SecretCinema()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/SecretCinema/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(3);
|
||||
releases.First().Should().BeOfType<GazelleInfo>();
|
||||
|
||||
var torrentInfo = releases.First() as GazelleInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("Singin' in the Rain (1952) 2160p");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("https://secret-cinema.pw/torrents.php?action=download&useToken=0&id=45068");
|
||||
torrentInfo.InfoUrl.Should().Be("https://secret-cinema.pw/torrents.php?id=2497&torrentid=45068");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-15 19:37:29"));
|
||||
torrentInfo.Size.Should().Be(57473058680);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
torrentInfo.Peers.Should().Be(8);
|
||||
torrentInfo.Seeders.Should().Be(8);
|
||||
torrentInfo.ImdbId.Should().Be(0);
|
||||
torrentInfo.TmdbId.Should().Be(0);
|
||||
torrentInfo.TvdbId.Should().Be(0);
|
||||
torrentInfo.Languages.Should().HaveCount(0);
|
||||
torrentInfo.Subs.Should().HaveCount(0);
|
||||
torrentInfo.DownloadVolumeFactor.Should().Be(0);
|
||||
torrentInfo.UploadVolumeFactor.Should().Be(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,12 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
};
|
||||
|
||||
_caps = new IndexerCapabilities();
|
||||
|
||||
_caps.Categories.AddCategoryMapping(2000, NewznabStandardCategory.Movies, "Movies");
|
||||
_caps.Categories.AddCategoryMapping(2040, NewznabStandardCategory.MoviesHD, "Movies/HD");
|
||||
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_caps);
|
||||
}
|
||||
|
||||
@@ -129,6 +133,38 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
releaseInfo.Peers.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_parse_recent_feed_from_torznab_morethantv()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_morethantv.xml");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
|
||||
|
||||
releases.Should().HaveCount(2);
|
||||
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
var releaseInfo = releases.First() as TorrentInfo;
|
||||
|
||||
releaseInfo.Title.Should().Be("Out of the Past 1947 720p BluRay FLAC2.0 x264-CtrlHD.mkv");
|
||||
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
releaseInfo.DownloadUrl.Should().Be("https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)");
|
||||
releaseInfo.InfoUrl.Should().Be("https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164");
|
||||
releaseInfo.CommentUrl.Should().Be("https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164");
|
||||
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
releaseInfo.PublishDate.Should().Be(DateTime.Parse("Tue, 20 Dec 2022 21:32:17 +0000").ToUniversalTime());
|
||||
releaseInfo.Size.Should().Be(5412993028);
|
||||
releaseInfo.TvdbId.Should().Be(0);
|
||||
releaseInfo.TvRageId.Should().Be(0);
|
||||
releaseInfo.InfoHash.Should().Be("(removed)");
|
||||
releaseInfo.Seeders.Should().Be(3);
|
||||
releaseInfo.Peers.Should().Be(3);
|
||||
releaseInfo.Categories.Count().Should().Be(4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_pagesize_reported_by_caps()
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="YamlDotNet" Version="12.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="12.3.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
||||
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
||||
status.Should().NotBeNull();
|
||||
status.DisabledTill.Should().HaveValue();
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
||||
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
||||
status.Should().NotBeNull();
|
||||
status.DisabledTill.Should().HaveValue();
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500);
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
||||
status.Should().NotBeNull();
|
||||
|
||||
origStatus.EscalationLevel.Should().Be(3);
|
||||
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
||||
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("TODO No Updates On Server")]
|
||||
public void finds_update_when_version_lower()
|
||||
{
|
||||
UseRealHttp();
|
||||
|
||||
@@ -74,6 +74,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
|
||||
}
|
||||
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
|
||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||
}
|
||||
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
@@ -126,6 +128,12 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
lidarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !lidarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Retain user settings not-affiliated with Prowlarr
|
||||
lidarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_lidarrV1Proxy.UpdateIndexer(lidarrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +167,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +184,11 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<LidarrField>()
|
||||
};
|
||||
|
||||
lidarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
@@ -191,7 +202,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||
{
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public int? DownloadClientId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public List<LidarrField> Fields { get; set; }
|
||||
|
||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -75,6 +75,8 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
|
||||
}
|
||||
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
|
||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||
}
|
||||
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
@@ -126,6 +128,12 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
radarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !radarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Retain user settings not-affiliated with Prowlarr
|
||||
radarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_radarrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +167,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +184,11 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<RadarrField>()
|
||||
};
|
||||
|
||||
radarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public int? DownloadClientId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public List<RadarrField> Fields { get; set; }
|
||||
|
||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||
}
|
||||
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
@@ -126,6 +128,8 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
readarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !readarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_readarrV1Proxy.UpdateIndexer(readarrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +163,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +180,11 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<ReadarrField>()
|
||||
};
|
||||
|
||||
readarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
@@ -191,7 +198,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||
{
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||
}
|
||||
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
@@ -126,6 +128,13 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
|
||||
{
|
||||
// Retain user fields not-affiliated with Prowlarr
|
||||
sonarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !sonarrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Retain user settings not-affiliated with Prowlarr
|
||||
sonarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
|
||||
sonarrIndexer.SeasonSearchMaximumSingleEpisodeAge = remoteIndexer.SeasonSearchMaximumSingleEpisodeAge;
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_sonarrV3Proxy.UpdateIndexer(sonarrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +168,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +185,11 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<SonarrField>()
|
||||
};
|
||||
|
||||
sonarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
@@ -189,7 +201,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
|
||||
return sonarrIndexer;
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public int? DownloadClientId { get; set; }
|
||||
public int? SeasonSearchMaximumSingleEpisodeAge { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public List<SonarrField> Fields { get; set; }
|
||||
|
||||
@@ -34,7 +36,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
var remoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||
}
|
||||
|
||||
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
@@ -126,6 +128,8 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
whisparrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !whisparrIndexer.Fields.Any(s => s.Name == f.Name)));
|
||||
|
||||
// Update the indexer if it still has categories that match
|
||||
_whisparrV3Proxy.UpdateIndexer(whisparrIndexer, Settings);
|
||||
}
|
||||
@@ -159,6 +163,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
@@ -175,9 +180,11 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
Fields = new List<WhisparrField>()
|
||||
};
|
||||
|
||||
whisparrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
|
||||
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiPathCompare = apiPath == otherApiPath;
|
||||
var apiPathCompare = apiPath.Equals(otherApiPath);
|
||||
|
||||
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
public enum AuthenticationRequiredType
|
||||
{
|
||||
Enabled = 0,
|
||||
DisabledForLocalAddresses = 1
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
namespace NzbDrone.Core.Authentication
|
||||
namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
public enum AuthenticationType
|
||||
{
|
||||
None = 0,
|
||||
Basic = 1,
|
||||
Forms = 2
|
||||
Forms = 2,
|
||||
External = 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Authentication
|
||||
@@ -8,5 +8,7 @@ namespace NzbDrone.Core.Authentication
|
||||
public Guid Identifier { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Salt { get; set; }
|
||||
public int Iterations { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -21,6 +23,10 @@ namespace NzbDrone.Core.Authentication
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
|
||||
private static readonly int ITERATIONS = 10000;
|
||||
private static readonly int SALT_SIZE = 128 / 8;
|
||||
private static readonly int NUMBER_OF_BYTES = 256 / 8;
|
||||
|
||||
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
|
||||
{
|
||||
_repo = repo;
|
||||
@@ -30,12 +36,15 @@ namespace NzbDrone.Core.Authentication
|
||||
|
||||
public User Add(string username, string password)
|
||||
{
|
||||
return _repo.Insert(new User
|
||||
var user = new User
|
||||
{
|
||||
Identifier = Guid.NewGuid(),
|
||||
Username = username.ToLowerInvariant(),
|
||||
Password = password.SHA256Hash()
|
||||
});
|
||||
Username = username.ToLowerInvariant()
|
||||
};
|
||||
|
||||
SetUserHashedPassword(user, password);
|
||||
|
||||
return _repo.Insert(user);
|
||||
}
|
||||
|
||||
public User Update(User user)
|
||||
@@ -54,7 +63,7 @@ namespace NzbDrone.Core.Authentication
|
||||
|
||||
if (user.Password != password)
|
||||
{
|
||||
user.Password = password.SHA256Hash();
|
||||
SetUserHashedPassword(user, password);
|
||||
}
|
||||
|
||||
user.Username = username.ToLowerInvariant();
|
||||
@@ -81,7 +90,20 @@ namespace NzbDrone.Core.Authentication
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user.Password == password.SHA256Hash())
|
||||
if (user.Salt.IsNullOrWhiteSpace())
|
||||
{
|
||||
// If password matches stored SHA256 hash, update to salted hash and verify.
|
||||
if (user.Password == password.SHA256Hash())
|
||||
{
|
||||
SetUserHashedPassword(user, password);
|
||||
|
||||
return Update(user);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (VerifyHashedPassword(user, password))
|
||||
{
|
||||
return user;
|
||||
}
|
||||
@@ -93,5 +115,42 @@ namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
return _repo.FindUser(identifier);
|
||||
}
|
||||
|
||||
private User SetUserHashedPassword(User user, string password)
|
||||
{
|
||||
var salt = GenerateSalt();
|
||||
|
||||
user.Iterations = ITERATIONS;
|
||||
user.Salt = Convert.ToBase64String(salt);
|
||||
user.Password = GetHashedPassword(password, salt, ITERATIONS);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private byte[] GenerateSalt()
|
||||
{
|
||||
var salt = new byte[SALT_SIZE];
|
||||
RandomNumberGenerator.Create().GetBytes(salt);
|
||||
|
||||
return salt;
|
||||
}
|
||||
|
||||
private string GetHashedPassword(string password, byte[] salt, int iterations)
|
||||
{
|
||||
return Convert.ToBase64String(KeyDerivation.Pbkdf2(
|
||||
password: password,
|
||||
salt: salt,
|
||||
prf: KeyDerivationPrf.HMACSHA512,
|
||||
iterationCount: iterations,
|
||||
numBytesRequested: NUMBER_OF_BYTES));
|
||||
}
|
||||
|
||||
private bool VerifyHashedPassword(User user, string password)
|
||||
{
|
||||
var salt = Convert.FromBase64String(user.Salt);
|
||||
var hashedPassword = GetHashedPassword(password, salt, user.Iterations);
|
||||
|
||||
return user.Password == hashedPassword;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace NzbDrone.Core.Configuration
|
||||
bool EnableSsl { get; }
|
||||
bool LaunchBrowser { get; }
|
||||
AuthenticationType AuthenticationMethod { get; }
|
||||
AuthenticationRequiredType AuthenticationRequired { get; }
|
||||
bool AnalyticsEnabled { get; }
|
||||
string LogLevel { get; }
|
||||
string ConsoleLogLevel { get; }
|
||||
@@ -193,10 +194,12 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
|
||||
|
||||
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
||||
|
||||
// TODO: Change back to "master" for the first stable release.
|
||||
public string Branch => GetValue("Branch", "develop").ToLowerInvariant();
|
||||
public string Branch => GetValue("Branch", "master").ToLowerInvariant();
|
||||
|
||||
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Configuration
|
||||
var releaseInfoPath = Path.Combine(bin, "release_info");
|
||||
|
||||
PackageUpdateMechanism = UpdateMechanism.BuiltIn;
|
||||
DefaultBranch = "develop";
|
||||
DefaultBranch = "master";
|
||||
|
||||
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(024)]
|
||||
public class add_salt_to_users : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Users")
|
||||
.AddColumn("Salt").AsString().Nullable()
|
||||
.AddColumn("Iterations").AsInt32().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(24)]
|
||||
public class newznab_yml : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Update.Table("Indexers").Set(new { Implementation = "GenericNewznab", ConfigContract = "GenericNewznabSettings" }).Where(new { Implementation = "Newznab" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public static class EncodingForBase64
|
||||
{
|
||||
public static string EncodeBase64(this string text)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] textAsBytes = System.Text.Encoding.UTF8.GetBytes(text);
|
||||
return System.Convert.ToBase64String(textAsBytes);
|
||||
}
|
||||
|
||||
public static string DecodeBase64(this string encodedText)
|
||||
{
|
||||
if (encodedText == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] textAsBytes = System.Convert.FromBase64String(encodedText);
|
||||
return System.Text.Encoding.UTF8.GetString(textAsBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public class FreeboxDownloadException : DownloadClientException
|
||||
{
|
||||
public FreeboxDownloadException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public enum FreeboxDownloadPriority
|
||||
{
|
||||
Last = 0,
|
||||
First = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public interface IFreeboxDownloadProxy
|
||||
{
|
||||
void Authenticate(FreeboxDownloadSettings settings);
|
||||
string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings);
|
||||
string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings);
|
||||
void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings);
|
||||
FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings);
|
||||
List<FreeboxDownloadTask> GetTasks(FreeboxDownloadSettings settings);
|
||||
}
|
||||
|
||||
public class FreeboxDownloadProxy : IFreeboxDownloadProxy
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
private ICached<string> _authSessionTokenCache;
|
||||
|
||||
public FreeboxDownloadProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_authSessionTokenCache = cacheManager.GetCache<string>(GetType(), "authSessionToken");
|
||||
}
|
||||
|
||||
public void Authenticate(FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/login").Build();
|
||||
|
||||
var response = ProcessRequest<FreeboxLogin>(request, settings);
|
||||
|
||||
if (response.Result.LoggedIn == false)
|
||||
{
|
||||
throw new DownloadClientAuthenticationException("Not logged");
|
||||
}
|
||||
}
|
||||
|
||||
public string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/add").Post();
|
||||
request.Headers.ContentType = "application/x-www-form-urlencoded";
|
||||
|
||||
request.AddFormParameter("download_url", System.Web.HttpUtility.UrlPathEncode(url));
|
||||
|
||||
if (!directory.IsNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("download_dir", directory);
|
||||
}
|
||||
|
||||
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
|
||||
|
||||
SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings);
|
||||
|
||||
return response.Result.Id;
|
||||
}
|
||||
|
||||
public string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/add").Post();
|
||||
|
||||
request.AddFormUpload("download_file", fileName, fileContent, "multipart/form-data");
|
||||
|
||||
if (directory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("download_dir", directory);
|
||||
}
|
||||
|
||||
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
|
||||
|
||||
SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings);
|
||||
|
||||
return response.Result.Id;
|
||||
}
|
||||
|
||||
public void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings)
|
||||
{
|
||||
var uri = "/downloads/" + id;
|
||||
|
||||
if (deleteData == true)
|
||||
{
|
||||
uri += "/erase";
|
||||
}
|
||||
|
||||
var request = BuildRequest(settings).Resource(uri).Build();
|
||||
|
||||
request.Method = HttpMethod.Delete;
|
||||
|
||||
ProcessRequest<string>(request, settings);
|
||||
}
|
||||
|
||||
public FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/config/").Build();
|
||||
|
||||
return ProcessRequest<FreeboxDownloadConfiguration>(request, settings).Result;
|
||||
}
|
||||
|
||||
public List<FreeboxDownloadTask> GetTasks(FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/").Build();
|
||||
|
||||
return ProcessRequest<List<FreeboxDownloadTask>>(request, settings).Result;
|
||||
}
|
||||
|
||||
private static string BuildCachedHeaderKey(FreeboxDownloadSettings settings)
|
||||
{
|
||||
return $"{settings.Host}:{settings.AppId}:{settings.AppToken}";
|
||||
}
|
||||
|
||||
private void SetTorrentSettings(string id, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/" + id).Build();
|
||||
|
||||
request.Method = HttpMethod.Put;
|
||||
|
||||
var body = new Dictionary<string, object> { };
|
||||
|
||||
if (addPaused)
|
||||
{
|
||||
body.Add("status", FreeboxDownloadTaskStatus.Stopped.ToString().ToLower());
|
||||
}
|
||||
|
||||
if (addFirst)
|
||||
{
|
||||
body.Add("queue_pos", "1");
|
||||
}
|
||||
|
||||
if (body.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
request.SetContent(body.ToJson());
|
||||
|
||||
ProcessRequest<FreeboxDownloadTask>(request, settings);
|
||||
}
|
||||
|
||||
private string GetSessionToken(HttpRequestBuilder requestBuilder, FreeboxDownloadSettings settings, bool force = false)
|
||||
{
|
||||
var sessionToken = _authSessionTokenCache.Find(BuildCachedHeaderKey(settings));
|
||||
|
||||
if (sessionToken == null || force)
|
||||
{
|
||||
_authSessionTokenCache.Remove(BuildCachedHeaderKey(settings));
|
||||
|
||||
_logger.Debug($"Client needs a new Session Token to reach the API with App ID '{settings.AppId}'");
|
||||
|
||||
// Obtaining a Session Token (from official documentation):
|
||||
// To protect the app_token secret, it will never be used directly to authenticate the
|
||||
// application, instead the API will provide a challenge the app will combine to its
|
||||
// app_token to open a session and get a session_token.
|
||||
// The validity of the session_token is limited in time and the app will have to renew
|
||||
// this session_token once in a while.
|
||||
|
||||
// Retrieving the 'challenge' value (it changes frequently and have a limited time validity)
|
||||
// needed to build password
|
||||
var challengeRequest = requestBuilder.Resource("/login").Build();
|
||||
challengeRequest.Method = HttpMethod.Get;
|
||||
|
||||
var challenge = ProcessRequest<FreeboxLogin>(challengeRequest, settings).Result.Challenge;
|
||||
|
||||
// The password is computed using the 'challenge' value and the 'app_token' ('App Token' setting)
|
||||
var enc = System.Text.Encoding.ASCII;
|
||||
var hmac = new HMACSHA1(enc.GetBytes(settings.AppToken));
|
||||
hmac.Initialize();
|
||||
var buffer = enc.GetBytes(challenge);
|
||||
var password = System.BitConverter.ToString(hmac.ComputeHash(buffer)).Replace("-", "").ToLower();
|
||||
|
||||
// Both 'app_id' ('App ID' setting) and computed password are set to get a Session Token
|
||||
var sessionRequest = requestBuilder.Resource("/login/session").Post().Build();
|
||||
var body = new Dictionary<string, object>
|
||||
{
|
||||
{ "app_id", settings.AppId },
|
||||
{ "password", password }
|
||||
};
|
||||
sessionRequest.SetContent(body.ToJson());
|
||||
|
||||
sessionToken = ProcessRequest<FreeboxLogin>(sessionRequest, settings).Result.SessionToken;
|
||||
|
||||
_authSessionTokenCache.Set(BuildCachedHeaderKey(settings), sessionToken);
|
||||
|
||||
_logger.Debug($"New Session Token stored in cache for App ID '{settings.AppId}', ready to reach API");
|
||||
}
|
||||
|
||||
return sessionToken;
|
||||
}
|
||||
|
||||
private HttpRequestBuilder BuildRequest(FreeboxDownloadSettings settings, bool authentication = true)
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.ApiUrl)
|
||||
{
|
||||
LogResponseContent = true
|
||||
};
|
||||
|
||||
requestBuilder.Headers.ContentType = "application/json";
|
||||
|
||||
if (authentication == true)
|
||||
{
|
||||
requestBuilder.SetHeader("X-Fbx-App-Auth", GetSessionToken(requestBuilder, settings));
|
||||
}
|
||||
|
||||
return requestBuilder;
|
||||
}
|
||||
|
||||
private FreeboxResponse<T> ProcessRequest<T>(HttpRequest request, FreeboxDownloadSettings settings)
|
||||
{
|
||||
request.LogResponseContent = true;
|
||||
request.SuppressHttpError = true;
|
||||
|
||||
HttpResponse response;
|
||||
|
||||
try
|
||||
{
|
||||
response = _httpClient.Execute(request);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new DownloadClientUnavailableException($"Unable to reach Freebox API. Verify 'Host', 'Port' or 'Use SSL' settings. (Error: {ex.Message})", ex);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
throw new DownloadClientUnavailableException("Unable to connect to Freebox API, please check your settings", ex);
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_authSessionTokenCache.Remove(BuildCachedHeaderKey(settings));
|
||||
|
||||
var responseContent = Json.Deserialize<FreeboxResponse<FreeboxLogin>>(response.Content);
|
||||
|
||||
var msg = $"Authentication to Freebox API failed. Reason: {responseContent.GetErrorDescription()}";
|
||||
_logger.Error(msg);
|
||||
throw new DownloadClientAuthenticationException(msg);
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new FreeboxDownloadException("Unable to reach Freebox API. Verify 'API URL' setting for base URL and version.");
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var responseContent = Json.Deserialize<FreeboxResponse<T>>(response.Content);
|
||||
|
||||
if (responseContent.Success)
|
||||
{
|
||||
return responseContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = $"Freebox API returned error: {responseContent.GetErrorDescription()}";
|
||||
_logger.Error(msg);
|
||||
throw new DownloadClientException(msg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DownloadClientException("Unable to connect to Freebox, please check your settings.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public class FreeboxDownloadSettingsValidator : AbstractValidator<FreeboxDownloadSettings>
|
||||
{
|
||||
public FreeboxDownloadSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Host).ValidHost();
|
||||
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
|
||||
RuleFor(c => c.ApiUrl).NotEmpty()
|
||||
.WithMessage("'API URL' must not be empty.");
|
||||
RuleFor(c => c.ApiUrl).ValidUrlBase();
|
||||
RuleFor(c => c.AppId).NotEmpty()
|
||||
.WithMessage("'App ID' must not be empty.");
|
||||
RuleFor(c => c.AppToken).NotEmpty()
|
||||
.WithMessage("'App Token' must not be empty.");
|
||||
RuleFor(c => c.Category).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase)
|
||||
.WithMessage("Allowed characters a-z and -");
|
||||
RuleFor(c => c.DestinationDirectory).IsValidPath()
|
||||
.When(c => c.DestinationDirectory.IsNotNullOrWhiteSpace());
|
||||
RuleFor(c => c.DestinationDirectory).Empty()
|
||||
.When(c => c.Category.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Cannot use 'Category' and 'Destination Directory' at the same time.");
|
||||
RuleFor(c => c.Category).Empty()
|
||||
.When(c => c.DestinationDirectory.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Cannot use 'Category' and 'Destination Directory' at the same time.");
|
||||
}
|
||||
}
|
||||
|
||||
public class FreeboxDownloadSettings : IProviderConfig
|
||||
{
|
||||
private static readonly FreeboxDownloadSettingsValidator Validator = new FreeboxDownloadSettingsValidator();
|
||||
|
||||
public FreeboxDownloadSettings()
|
||||
{
|
||||
Host = "mafreebox.freebox.fr";
|
||||
Port = 443;
|
||||
UseSsl = true;
|
||||
ApiUrl = "/api/v1/";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox, HelpText = "Hostname or host IP address of the Freebox, defaults to 'mafreebox.freebox.fr' (will only work if on same network)")]
|
||||
public string Host { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "Port used to access Freebox interface, defaults to '443'")]
|
||||
public int Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secured connection when connecting to Freebox API")]
|
||||
public bool UseSsl { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "API URL", Type = FieldType.Textbox, Advanced = true, HelpText = "Define Freebox API base URL with API version, eg http://[host]:[port]/[api_base_url]/[api_version]/, defaults to '/api/v1/'")]
|
||||
public string ApiUrl { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "App ID", Type = FieldType.Textbox, HelpText = "App ID given when creating access to Freebox API (ie 'app_id')")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "App Token", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "App token retrieved when creating access to Freebox API (ie 'app_token')")]
|
||||
public string AppToken { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Destination Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Freebox download location")]
|
||||
public string DestinationDirectory { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated non-Prowlarr downloads (will create a [category] subdirectory in the output directory)")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing")]
|
||||
public int Priority { get; set; }
|
||||
|
||||
[FieldDefinition(10, Label = "Add Paused", Type = FieldType.Checkbox)]
|
||||
public bool AddPaused { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||
{
|
||||
public class FreeboxDownloadConfiguration
|
||||
{
|
||||
[JsonProperty(PropertyName = "download_dir")]
|
||||
public string DownloadDirectory { get; set; }
|
||||
public string DecodedDownloadDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return DownloadDirectory.DecodeBase64();
|
||||
}
|
||||
set
|
||||
{
|
||||
DownloadDirectory = value.EncodeBase64();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||
{
|
||||
public enum FreeboxDownloadTaskType
|
||||
{
|
||||
Bt,
|
||||
Nzb,
|
||||
Http,
|
||||
Ftp
|
||||
}
|
||||
|
||||
public enum FreeboxDownloadTaskStatus
|
||||
{
|
||||
Unknown,
|
||||
Stopped,
|
||||
Queued,
|
||||
Starting,
|
||||
Downloading,
|
||||
Stopping,
|
||||
Error,
|
||||
Done,
|
||||
Checking,
|
||||
Repairing,
|
||||
Extracting,
|
||||
Seeding,
|
||||
Retry
|
||||
}
|
||||
|
||||
public enum FreeboxDownloadTaskIoPriority
|
||||
{
|
||||
Low,
|
||||
Normal,
|
||||
High
|
||||
}
|
||||
|
||||
public class FreeboxDownloadTask
|
||||
{
|
||||
private static readonly Dictionary<string, string> Descriptions;
|
||||
|
||||
[JsonProperty(PropertyName = "id")]
|
||||
public string Id { get; set; }
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty(PropertyName = "download_dir")]
|
||||
public string DownloadDirectory { get; set; }
|
||||
public string DecodedDownloadDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return DownloadDirectory.DecodeBase64();
|
||||
}
|
||||
set
|
||||
{
|
||||
DownloadDirectory = value.EncodeBase64();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "info_hash")]
|
||||
public string InfoHash { get; set; }
|
||||
[JsonProperty(PropertyName = "queue_pos")]
|
||||
public int QueuePosition { get; set; }
|
||||
[JsonConverter(typeof(UnderscoreStringEnumConverter), FreeboxDownloadTaskStatus.Unknown)]
|
||||
public FreeboxDownloadTaskStatus Status { get; set; }
|
||||
[JsonProperty(PropertyName = "eta")]
|
||||
public long Eta { get; set; }
|
||||
[JsonProperty(PropertyName = "error")]
|
||||
public string Error { get; set; }
|
||||
[JsonProperty(PropertyName = "type")]
|
||||
public string Type { get; set; }
|
||||
[JsonProperty(PropertyName = "io_priority")]
|
||||
public string IoPriority { get; set; }
|
||||
[JsonProperty(PropertyName = "stop_ratio")]
|
||||
public long StopRatio { get; set; }
|
||||
[JsonProperty(PropertyName = "piece_length")]
|
||||
public long PieceLength { get; set; }
|
||||
[JsonProperty(PropertyName = "created_ts")]
|
||||
public long CreatedTimestamp { get; set; }
|
||||
[JsonProperty(PropertyName = "size")]
|
||||
public long Size { get; set; }
|
||||
[JsonProperty(PropertyName = "rx_pct")]
|
||||
public long ReceivedPrct { get; set; }
|
||||
[JsonProperty(PropertyName = "rx_bytes")]
|
||||
public long ReceivedBytes { get; set; }
|
||||
[JsonProperty(PropertyName = "rx_rate")]
|
||||
public long ReceivedRate { get; set; }
|
||||
[JsonProperty(PropertyName = "tx_pct")]
|
||||
public long TransmittedPrct { get; set; }
|
||||
[JsonProperty(PropertyName = "tx_bytes")]
|
||||
public long TransmittedBytes { get; set; }
|
||||
[JsonProperty(PropertyName = "tx_rate")]
|
||||
public long TransmittedRate { get; set; }
|
||||
|
||||
static FreeboxDownloadTask()
|
||||
{
|
||||
Descriptions = new Dictionary<string, string>
|
||||
{
|
||||
{ "internal", "Internal error." },
|
||||
{ "disk_full", "The disk is full." },
|
||||
{ "unknown", "Unknown error." },
|
||||
{ "parse_error", "Parse error." },
|
||||
{ "unknown_host", "Unknown host." },
|
||||
{ "timeout", "Timeout." },
|
||||
{ "bad_authentication", "Invalid credentials." },
|
||||
{ "connection_refused", "Remote host refused connection." },
|
||||
{ "bt_tracker_error", "Unable to announce on tracker." },
|
||||
{ "bt_missing_files", "Missing torrent files." },
|
||||
{ "bt_file_error", "Error accessing torrent files." },
|
||||
{ "missing_ctx_file", "Error accessing task context file." },
|
||||
{ "nzb_no_group", "Cannot find the requested group on server." },
|
||||
{ "nzb_not_found", "Article not fount on the server." },
|
||||
{ "nzb_invalid_crc", "Invalid article CRC." },
|
||||
{ "nzb_invalid_size", "Invalid article size." },
|
||||
{ "nzb_invalid_filename", "Invalid filename." },
|
||||
{ "nzb_open_failed", "Error opening." },
|
||||
{ "nzb_write_failed", "Error writing." },
|
||||
{ "nzb_missing_size", "Missing article size." },
|
||||
{ "nzb_decode_error", "Article decoding error." },
|
||||
{ "nzb_missing_segments", "Missing article segments." },
|
||||
{ "nzb_error", "Other nzb error." },
|
||||
{ "nzb_authentication_required", "Nzb server need authentication." }
|
||||
};
|
||||
}
|
||||
|
||||
public string GetErrorDescription()
|
||||
{
|
||||
if (Descriptions.ContainsKey(Error))
|
||||
{
|
||||
return Descriptions[Error];
|
||||
}
|
||||
|
||||
return $"{Error} - Unknown error";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||
{
|
||||
public class FreeboxLogin
|
||||
{
|
||||
[JsonProperty(PropertyName = "logged_in")]
|
||||
public bool LoggedIn { get; set; }
|
||||
[JsonProperty(PropertyName = "challenge")]
|
||||
public string Challenge { get; set; }
|
||||
[JsonProperty(PropertyName = "password_salt")]
|
||||
public string PasswordSalt { get; set; }
|
||||
[JsonProperty(PropertyName = "password_set")]
|
||||
public bool PasswordSet { get; set; }
|
||||
[JsonProperty(PropertyName = "session_token")]
|
||||
public string SessionToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||
{
|
||||
public class FreeboxResponse<T>
|
||||
{
|
||||
private static readonly Dictionary<string, string> Descriptions;
|
||||
|
||||
[JsonProperty(PropertyName = "success")]
|
||||
public bool Success { get; set; }
|
||||
[JsonProperty(PropertyName = "msg")]
|
||||
public string Message { get; set; }
|
||||
[JsonProperty(PropertyName = "error_code")]
|
||||
public string ErrorCode { get; set; }
|
||||
[JsonProperty(PropertyName = "result")]
|
||||
public T Result { get; set; }
|
||||
|
||||
static FreeboxResponse()
|
||||
{
|
||||
Descriptions = new Dictionary<string, string>
|
||||
{
|
||||
// Common errors
|
||||
{ "invalid_request", "Your request is invalid." },
|
||||
{ "invalid_api_version", "Invalid API base url or unknown API version." },
|
||||
{ "internal_error", "Internal error." },
|
||||
|
||||
// Login API errors
|
||||
{ "auth_required", "Invalid session token, or no session token sent." },
|
||||
{ "invalid_token", "The app token you are trying to use is invalid or has been revoked." },
|
||||
{ "pending_token", "The app token you are trying to use has not been validated by user yet." },
|
||||
{ "insufficient_rights", "Your app permissions does not allow accessing this API." },
|
||||
{ "denied_from_external_ip", "You are trying to get an app_token from a remote IP." },
|
||||
{ "ratelimited", "Too many auth error have been made from your IP." },
|
||||
{ "new_apps_denied", "New application token request has been disabled." },
|
||||
{ "apps_denied", "API access from apps has been disabled." },
|
||||
|
||||
// Download API errors
|
||||
{ "task_not_found", "No task was found with the given id." },
|
||||
{ "invalid_operation", "Attempt to perform an invalid operation." },
|
||||
{ "invalid_file", "Error with the download file (invalid format ?)." },
|
||||
{ "invalid_url", "URL is invalid." },
|
||||
{ "not_implemented", "Method not implemented." },
|
||||
{ "out_of_memory", "No more memory available to perform the requested action." },
|
||||
{ "invalid_task_type", "The task type is invalid." },
|
||||
{ "hibernating", "The downloader is hibernating." },
|
||||
{ "need_bt_stopped_done", "This action is only valid for Bittorrent task in stopped or done state." },
|
||||
{ "bt_tracker_not_found", "Attempt to access an invalid tracker object." },
|
||||
{ "too_many_tasks", "Too many tasks." },
|
||||
{ "invalid_address", "Invalid peer address." },
|
||||
{ "port_conflict", "Port conflict when setting config." },
|
||||
{ "invalid_priority", "Invalid priority." },
|
||||
{ "ctx_file_error", "Failed to initialize task context file (need to check disk)." },
|
||||
{ "exists", "Same task already exists." },
|
||||
{ "port_outside_range", "Incoming port is not available for this customer." }
|
||||
};
|
||||
}
|
||||
|
||||
public string GetErrorDescription()
|
||||
{
|
||||
if (Descriptions.ContainsKey(ErrorCode))
|
||||
{
|
||||
return Descriptions[ErrorCode];
|
||||
}
|
||||
|
||||
return $"{ErrorCode} - Unknown error";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public class TorrentFreeboxDownload : TorrentClientBase<FreeboxDownloadSettings>
|
||||
{
|
||||
private readonly IFreeboxDownloadProxy _proxy;
|
||||
|
||||
public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy,
|
||||
ITorrentFileInfoReader torrentFileInfoReader,
|
||||
IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
public override string Name => "Freebox Download";
|
||||
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
protected IEnumerable<FreeboxDownloadTask> GetTorrents()
|
||||
{
|
||||
return _proxy.GetTasks(Settings).Where(v => v.Type.ToLower() == FreeboxDownloadTaskType.Bt.ToString().ToLower());
|
||||
}
|
||||
|
||||
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
|
||||
{
|
||||
return _proxy.AddTaskFromUrl(magnetLink,
|
||||
GetDownloadDirectory(release).EncodeBase64(),
|
||||
ToBePaused(),
|
||||
ToBeQueuedFirst(),
|
||||
Settings);
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
|
||||
{
|
||||
return _proxy.AddTaskFromFile(filename,
|
||||
fileContent,
|
||||
GetDownloadDirectory(release).EncodeBase64(),
|
||||
ToBePaused(),
|
||||
ToBeQueuedFirst(),
|
||||
Settings);
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
|
||||
{
|
||||
return _proxy.AddTaskFromUrl(torrentLink,
|
||||
GetDownloadDirectory(release).EncodeBase64(),
|
||||
ToBePaused(),
|
||||
ToBeQueuedFirst(),
|
||||
Settings);
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
try
|
||||
{
|
||||
_proxy.Authenticate(Settings);
|
||||
}
|
||||
catch (DownloadClientUnavailableException ex)
|
||||
{
|
||||
failures.Add(new ValidationFailure("Host", ex.Message));
|
||||
failures.Add(new ValidationFailure("Port", ex.Message));
|
||||
}
|
||||
catch (DownloadClientAuthenticationException ex)
|
||||
{
|
||||
failures.Add(new ValidationFailure("AppId", ex.Message));
|
||||
failures.Add(new ValidationFailure("AppToken", ex.Message));
|
||||
}
|
||||
catch (FreeboxDownloadException ex)
|
||||
{
|
||||
failures.Add(new ValidationFailure("ApiUrl", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ValidateCategories(List<ValidationFailure> failures)
|
||||
{
|
||||
base.ValidateCategories(failures);
|
||||
|
||||
foreach (var label in Categories)
|
||||
{
|
||||
if (!Regex.IsMatch(label.ClientCategory, "^\\.?[-a-z]*$"))
|
||||
{
|
||||
failures.AddIfNotNull(new ValidationFailure(string.Empty, "Mapped Categories allowed characters a-z and -"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetDownloadDirectory(ReleaseInfo release)
|
||||
{
|
||||
if (Settings.DestinationDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return Settings.DestinationDirectory.TrimEnd('/');
|
||||
}
|
||||
|
||||
var destDir = _proxy.GetDownloadConfiguration(Settings).DecodedDownloadDirectory.TrimEnd('/');
|
||||
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
|
||||
destDir = $"{destDir}/{category}";
|
||||
}
|
||||
|
||||
return destDir;
|
||||
}
|
||||
|
||||
private bool ToBeQueuedFirst()
|
||||
{
|
||||
if (Settings.Priority == (int)FreeboxDownloadPriority.First)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ToBePaused()
|
||||
{
|
||||
return Settings.AddPaused;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
|
||||
public class IndexerCheck : HealthCheckBase
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
|
||||
public class IndexerLongTermStatusCheck : HealthCheckBase
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
|
||||
public class IndexerStatusCheck : HealthCheckBase
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class IndexerVIPCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class IndexerVIPExpiredCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
@@ -9,6 +9,7 @@ using NzbDrone.Core.ThingiProvider.Events;
|
||||
namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class NoDefinitionCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;
|
||||
|
||||
@@ -9,6 +9,7 @@ using NzbDrone.Core.ThingiProvider.Events;
|
||||
namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
|
||||
public class OutdatedDefinitionCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public enum ReleaseBranches
|
||||
{
|
||||
Master,
|
||||
Develop,
|
||||
Nightly
|
||||
}
|
||||
|
||||
@@ -130,6 +130,8 @@ namespace NzbDrone.Core.History
|
||||
history.Data.Add("ImdbId", ((MovieSearchCriteria)message.Query).FullImdbId ?? string.Empty);
|
||||
history.Data.Add("TmdbId", ((MovieSearchCriteria)message.Query).TmdbId?.ToString() ?? string.Empty);
|
||||
history.Data.Add("TraktId", ((MovieSearchCriteria)message.Query).TraktId?.ToString() ?? string.Empty);
|
||||
history.Data.Add("Year", ((MovieSearchCriteria)message.Query).Year?.ToString() ?? string.Empty);
|
||||
history.Data.Add("Genre", ((MovieSearchCriteria)message.Query).Genre ?? string.Empty);
|
||||
}
|
||||
|
||||
if (message.Query is TvSearchCriteria)
|
||||
@@ -142,18 +144,27 @@ namespace NzbDrone.Core.History
|
||||
history.Data.Add("TvMazeId", ((TvSearchCriteria)message.Query).TvMazeId?.ToString() ?? string.Empty);
|
||||
history.Data.Add("Season", ((TvSearchCriteria)message.Query).Season?.ToString() ?? string.Empty);
|
||||
history.Data.Add("Episode", ((TvSearchCriteria)message.Query).Episode ?? string.Empty);
|
||||
history.Data.Add("Year", ((TvSearchCriteria)message.Query).Year?.ToString() ?? string.Empty);
|
||||
history.Data.Add("Genre", ((TvSearchCriteria)message.Query).Genre ?? string.Empty);
|
||||
}
|
||||
|
||||
if (message.Query is MusicSearchCriteria)
|
||||
{
|
||||
history.Data.Add("Artist", ((MusicSearchCriteria)message.Query).Artist ?? string.Empty);
|
||||
history.Data.Add("Album", ((MusicSearchCriteria)message.Query).Album ?? string.Empty);
|
||||
history.Data.Add("Track", ((MusicSearchCriteria)message.Query).Track ?? string.Empty);
|
||||
history.Data.Add("Label", ((MusicSearchCriteria)message.Query).Label ?? string.Empty);
|
||||
history.Data.Add("Year", ((MusicSearchCriteria)message.Query).Year?.ToString() ?? string.Empty);
|
||||
history.Data.Add("Genre", ((MusicSearchCriteria)message.Query).Genre ?? string.Empty);
|
||||
}
|
||||
|
||||
if (message.Query is BookSearchCriteria)
|
||||
{
|
||||
history.Data.Add("Author", ((BookSearchCriteria)message.Query).Author ?? string.Empty);
|
||||
history.Data.Add("BookTitle", ((BookSearchCriteria)message.Query).Title ?? string.Empty);
|
||||
history.Data.Add("Publisher", ((BookSearchCriteria)message.Query).Publisher ?? string.Empty);
|
||||
history.Data.Add("Year", ((BookSearchCriteria)message.Query).Year?.ToString() ?? string.Empty);
|
||||
history.Data.Add("Genre", ((BookSearchCriteria)message.Query).Genre ?? string.Empty);
|
||||
}
|
||||
|
||||
history.Data.Add("ElapsedTime", message.QueryResult.Response?.ElapsedTime.ToString() ?? string.Empty);
|
||||
|
||||
@@ -28,7 +28,11 @@ namespace NzbDrone.Core.Http.CloudFlare
|
||||
if (response.StatusCode.Equals(HttpStatusCode.ServiceUnavailable) ||
|
||||
response.StatusCode.Equals(HttpStatusCode.Forbidden))
|
||||
{
|
||||
return true; // Defected CloudFlare and DDoS-GUARD
|
||||
var responseHtml = response.Content;
|
||||
if (responseHtml.Contains("<title>Just a moment...") || responseHtml.Contains("<title>DDOS-GUARD"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public class NewznabRequest
|
||||
{
|
||||
private static readonly Regex TvRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex MusicRegex = new Regex(@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:track\:)(?<track>[^{]+)|(?:label\:)(?<label>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:publisher\:)(?<publisher>[^{]+)|(?:title\:)(?<title>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex TvRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:rid\:)(?<rid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:traktid\:)(?<traktid>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex MusicRegex = new Regex(@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:track\:)(?<track>[^{]+)|(?:label\:)(?<label>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:publisher\:)(?<publisher>[^{]+)|(?:title\:)(?<title>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public string t { get; set; }
|
||||
public string q { get; set; }
|
||||
@@ -61,6 +61,11 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var tmdb) ? tmdb : null;
|
||||
}
|
||||
|
||||
if (match.Groups["rid"].Success)
|
||||
{
|
||||
rid = int.TryParse(match.Groups["rid"].Value, out var rId) ? rId : null;
|
||||
}
|
||||
|
||||
if (match.Groups["season"].Success)
|
||||
{
|
||||
season = int.TryParse(match.Groups["season"].Value, out var seasonParsed) ? seasonParsed : null;
|
||||
@@ -71,11 +76,26 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
imdbid = match.Groups["imdbid"].Value;
|
||||
}
|
||||
|
||||
if (match.Groups["traktid"].Success)
|
||||
{
|
||||
traktid = int.TryParse(match.Groups["traktid"].Value, out var trackId) ? trackId : null;
|
||||
}
|
||||
|
||||
if (match.Groups["episode"].Success)
|
||||
{
|
||||
ep = match.Groups["episode"].Value;
|
||||
}
|
||||
|
||||
if (match.Groups["year"].Success)
|
||||
{
|
||||
year = int.TryParse(match.Groups["year"].Value, out var parsedYear) ? parsedYear : null;
|
||||
}
|
||||
|
||||
if (match.Groups["genre"].Success)
|
||||
{
|
||||
genre = match.Groups["genre"].Value;
|
||||
}
|
||||
|
||||
q = q.Replace(match.Value, "");
|
||||
}
|
||||
}
|
||||
@@ -93,7 +113,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
if (match.Groups["doubanid"].Success)
|
||||
{
|
||||
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var tmdb) ? tmdb : null;
|
||||
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var doubanId) ? doubanId : null;
|
||||
}
|
||||
|
||||
if (match.Groups["imdbid"].Success)
|
||||
@@ -101,6 +121,21 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
imdbid = match.Groups["imdbid"].Value;
|
||||
}
|
||||
|
||||
if (match.Groups["traktid"].Success)
|
||||
{
|
||||
traktid = int.TryParse(match.Groups["traktid"].Value, out var trackId) ? trackId : null;
|
||||
}
|
||||
|
||||
if (match.Groups["year"].Success)
|
||||
{
|
||||
year = int.TryParse(match.Groups["year"].Value, out var parsedYear) ? parsedYear : null;
|
||||
}
|
||||
|
||||
if (match.Groups["genre"].Success)
|
||||
{
|
||||
genre = match.Groups["genre"].Value;
|
||||
}
|
||||
|
||||
q = q.Replace(match.Value, "").Trim();
|
||||
}
|
||||
}
|
||||
@@ -131,6 +166,16 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
label = match.Groups["label"].Value;
|
||||
}
|
||||
|
||||
if (match.Groups["year"].Success)
|
||||
{
|
||||
year = int.TryParse(match.Groups["year"].Value, out var parsedYear) ? parsedYear : null;
|
||||
}
|
||||
|
||||
if (match.Groups["genre"].Success)
|
||||
{
|
||||
genre = match.Groups["genre"].Value;
|
||||
}
|
||||
|
||||
q = q.Replace(match.Value, "").Trim();
|
||||
}
|
||||
}
|
||||
@@ -156,6 +201,16 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
publisher = match.Groups["publisher"].Value;
|
||||
}
|
||||
|
||||
if (match.Groups["year"].Success)
|
||||
{
|
||||
year = int.TryParse(match.Groups["year"].Value, out var parsedYear) ? parsedYear : null;
|
||||
}
|
||||
|
||||
if (match.Groups["genre"].Success)
|
||||
{
|
||||
genre = match.Groups["genre"].Value;
|
||||
}
|
||||
|
||||
q = q.Replace(match.Value, "").Trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +93,13 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c, protocol),
|
||||
r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c, protocol),
|
||||
r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol),
|
||||
GetNabElement("rageid", r.TvRageId, protocol),
|
||||
GetNabElement("tvdbid", r.TvdbId, protocol),
|
||||
GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
|
||||
GetNabElement("tmdbid", r.TmdbId, protocol),
|
||||
GetNabElement("traktid", r.TraktId, protocol),
|
||||
GetNabElement("doubanid", r.DoubanId, protocol),
|
||||
r.TvRageId == 0 ? null : GetNabElement("rageid", r.TvRageId, protocol),
|
||||
r.TvdbId == 0 ? null : GetNabElement("tvdbid", r.TvdbId, protocol),
|
||||
r.ImdbId == 0 ? null : GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
|
||||
r.TmdbId == 0 ? null : GetNabElement("tmdbid", r.TmdbId, protocol),
|
||||
r.TraktId == 0 ? null : GetNabElement("traktid", r.TraktId, protocol),
|
||||
r.TvMazeId == 0 ? null : GetNabElement("tvmazeid", r.TvMazeId, protocol),
|
||||
r.DoubanId == 0 ? null : GetNabElement("doubanid", r.DoubanId, protocol),
|
||||
GetNabElement("seeders", t.Seeders, protocol),
|
||||
GetNabElement("files", r.Files, protocol),
|
||||
GetNabElement("grabs", r.Grabs, protocol),
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Analytics;
|
||||
using NzbDrone.Core.Indexers.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public class ReleaseAnalyticsService : IHandleAsync<IndexerQueryEvent>
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IHttpRequestBuilderFactory _requestBuilder;
|
||||
private readonly IAnalyticsService _analyticsService;
|
||||
private readonly Debouncer _debouncer;
|
||||
private readonly Logger _logger;
|
||||
private readonly List<ReleaseInfo> _pendingUpdates;
|
||||
|
||||
public ReleaseAnalyticsService(IHttpClient httpClient, IProwlarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, Logger logger)
|
||||
{
|
||||
_debouncer = new Debouncer(SendReleases, TimeSpan.FromMinutes(10));
|
||||
_analyticsService = analyticsService;
|
||||
_requestBuilder = requestBuilder.Releases;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_pendingUpdates = new List<ReleaseInfo>();
|
||||
}
|
||||
|
||||
public void HandleAsync(IndexerQueryEvent message)
|
||||
{
|
||||
if (_analyticsService.IsEnabled && message.QueryResult?.Releases != null)
|
||||
{
|
||||
lock (_pendingUpdates)
|
||||
{
|
||||
_pendingUpdates.AddRange(message.QueryResult.Releases.Where(r => r.Title.IsNotNullOrWhiteSpace()));
|
||||
}
|
||||
|
||||
_debouncer.Execute();
|
||||
}
|
||||
}
|
||||
|
||||
public void SendReleases()
|
||||
{
|
||||
lock (_pendingUpdates)
|
||||
{
|
||||
var pendingUpdates = _pendingUpdates.ToArray();
|
||||
_pendingUpdates.Clear();
|
||||
|
||||
var request = _requestBuilder.Create().Resource("release/push").Build();
|
||||
request.Method = HttpMethod.Post;
|
||||
request.Headers.ContentType = "application/json";
|
||||
request.SuppressHttpError = true;
|
||||
request.LogHttpError = false;
|
||||
|
||||
var body = pendingUpdates.DistinctBy(r => r.Title).Select(x => new
|
||||
{
|
||||
Title = x.Title,
|
||||
Categories = x.Categories?.Where(c => c.Id < 10000).Select(c => c.Id) ?? new List<int>(),
|
||||
Protocol = x.DownloadProtocol.ToString(),
|
||||
Size = x.Size,
|
||||
PublishDate = x.PublishDate
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
request.SetContent(body.ToJson());
|
||||
_httpClient.Post(request);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.Trace("Analytics push failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
{
|
||||
public interface IIndexerDefinitionUpdateService
|
||||
{
|
||||
List<IndexerMetaDefinition> All();
|
||||
List<IndexerMetaDefinition> AllForImplementation(string implementation);
|
||||
List<CardigannMetaDefinition> All();
|
||||
CardigannDefinition GetCachedDefinition(string fileKey);
|
||||
List<string> GetBlocklist();
|
||||
}
|
||||
@@ -29,8 +28,8 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
{
|
||||
/* Update Service will fall back if version # does not exist for an indexer per Ta */
|
||||
|
||||
private const string DEFINITION_BRANCH = "newznab-yml";
|
||||
private const int DEFINITION_VERSION = 8;
|
||||
private const string DEFINITION_BRANCH = "master";
|
||||
private const int DEFINITION_VERSION = 7;
|
||||
|
||||
//Used when moving yml to C#
|
||||
private readonly List<string> _defintionBlocklist = new List<string>()
|
||||
@@ -79,9 +78,9 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<IndexerMetaDefinition> All()
|
||||
public List<CardigannMetaDefinition> All()
|
||||
{
|
||||
var indexerList = new List<IndexerMetaDefinition>();
|
||||
var indexerList = new List<CardigannMetaDefinition>();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -89,7 +88,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
try
|
||||
{
|
||||
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
|
||||
var response = _httpClient.Get<List<IndexerMetaDefinition>>(request);
|
||||
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
|
||||
indexerList = response.Resource.Where(i => !_defintionBlocklist.Contains(i.File)).ToList();
|
||||
}
|
||||
catch
|
||||
@@ -112,11 +111,6 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
return indexerList;
|
||||
}
|
||||
|
||||
public List<IndexerMetaDefinition> AllForImplementation(string implementation)
|
||||
{
|
||||
return All().Where(d => d.Implementation == implementation.ToLower()).ToList();
|
||||
}
|
||||
|
||||
public CardigannDefinition GetCachedDefinition(string fileKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileKey))
|
||||
@@ -134,7 +128,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
return _defintionBlocklist;
|
||||
}
|
||||
|
||||
private List<IndexerMetaDefinition> ReadDefinitionsFromDisk(List<IndexerMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
|
||||
private List<CardigannMetaDefinition> ReadDefinitionsFromDisk(List<CardigannMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
|
||||
{
|
||||
var indexerList = defs;
|
||||
|
||||
@@ -151,7 +145,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
try
|
||||
{
|
||||
var definitionString = File.ReadAllText(file.FullName);
|
||||
var definition = _deserializer.Deserialize<IndexerMetaDefinition>(definitionString);
|
||||
var definition = _deserializer.Deserialize<CardigannMetaDefinition>(definitionString);
|
||||
|
||||
definition.File = Path.GetFileNameWithoutExtension(file.Name);
|
||||
|
||||
@@ -249,11 +243,6 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
definition.Login.Method = "form";
|
||||
}
|
||||
|
||||
if (definition.Search == null)
|
||||
{
|
||||
definition.Search = new SearchBlock();
|
||||
}
|
||||
|
||||
if (definition.Search.Paths == null)
|
||||
{
|
||||
definition.Search.Paths = new List<SearchPathBlock>();
|
||||
|
||||
@@ -98,6 +98,12 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
var jsonResponse = new HttpResponse<AvistazErrorResponse>(ex.Response);
|
||||
return new ValidationFailure(string.Empty, jsonResponse.Resource?.Message ?? "Unauthorized request to indexer");
|
||||
}
|
||||
else if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.Warn(ex, "Too Many Requests");
|
||||
|
||||
return new ValidationFailure(string.Empty, "Too Many Requests");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user