mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
1 Commits
v1.1.2.245
...
cardigann-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c45eb68fa |
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.1.2'
|
||||
majorVersion: '0.4.11'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.405'
|
||||
dotnetVersion: '6.0.301'
|
||||
innoVersion: '6.2.0'
|
||||
nodeVersion: '16.x'
|
||||
windowsImage: 'windows-2022'
|
||||
|
||||
@@ -142,8 +142,8 @@ module.exports = (env) => {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /[\\/]node_modules[\\/](?!(@sentry\/browser|@sentry\/integrations|chart.js|filesize|normalize.css)[\\/])/,
|
||||
test: /\.js?$/,
|
||||
exclude: /(node_modules|JsLibraries)/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
|
||||
@@ -36,6 +36,7 @@ class TagInputInput extends Component {
|
||||
<div
|
||||
ref={forwardedRef}
|
||||
className={className}
|
||||
component="div"
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
/** Outline **/
|
||||
|
||||
.outline {
|
||||
background-color: var(--cardBackgroundColor);
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
@@ -108,5 +108,5 @@
|
||||
/** Outline **/
|
||||
|
||||
.outline {
|
||||
background-color: var(--cardBackgroundColor);
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
color: #515253;
|
||||
color: var(--toobarButtonHoverColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -76,7 +75,6 @@ class Page extends Component {
|
||||
isSmallScreen,
|
||||
isSidebarVisible,
|
||||
enableColorImpairedMode,
|
||||
authenticationEnabled,
|
||||
onSidebarToggle,
|
||||
onSidebarVisibleChange
|
||||
} = this.props;
|
||||
@@ -111,10 +109,6 @@ class Page extends Component {
|
||||
isOpen={this.state.isConnectionLostModalOpen}
|
||||
onModalClose={this.onConnectionLostModalClose}
|
||||
/>
|
||||
|
||||
<AuthenticationRequiredModal
|
||||
isOpen={!authenticationEnabled}
|
||||
/>
|
||||
</div>
|
||||
</ColorImpairedContext.Provider>
|
||||
);
|
||||
@@ -130,7 +124,6 @@ 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,7 +11,6 @@ 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';
|
||||
@@ -134,21 +133,18 @@ function createMapStateToProps() {
|
||||
selectErrors,
|
||||
selectAppProps,
|
||||
createDimensionsSelector(),
|
||||
createSystemStatusSelector(),
|
||||
(
|
||||
enableColorImpairedMode,
|
||||
isPopulated,
|
||||
errors,
|
||||
app,
|
||||
dimensions,
|
||||
systemStatus
|
||||
dimensions
|
||||
) => {
|
||||
return {
|
||||
...app,
|
||||
...errors,
|
||||
isPopulated,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||
enableColorImpairedMode
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
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;
|
||||
@@ -1,5 +0,0 @@
|
||||
.authRequiredAlert {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
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;
|
||||
@@ -1,86 +0,0 @@
|
||||
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,42 +226,6 @@ 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
|
||||
@@ -279,15 +243,6 @@ 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()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -272,7 +272,6 @@ class IndexerIndex extends Component {
|
||||
saveError,
|
||||
isDeleting,
|
||||
isTestingAll,
|
||||
isSyncingIndexers,
|
||||
deleteError,
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
@@ -310,15 +309,6 @@ 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}
|
||||
@@ -503,12 +493,10 @@ 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,13 +2,10 @@ 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';
|
||||
@@ -16,16 +13,13 @@ 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
|
||||
};
|
||||
}
|
||||
@@ -52,12 +46,6 @@ 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) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
|
||||
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id));
|
||||
}
|
||||
|
||||
const nameList = filteredList.map((item) => item.name).sort();
|
||||
|
||||
@@ -79,7 +79,6 @@ class IndexerIndexRow extends Component {
|
||||
privacy,
|
||||
priority,
|
||||
status,
|
||||
fields,
|
||||
appProfile,
|
||||
added,
|
||||
capabilities,
|
||||
@@ -97,8 +96,6 @@ class IndexerIndexRow extends Component {
|
||||
isIndexerInfoModalOpen
|
||||
} = this.state;
|
||||
|
||||
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? indexerUrls[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
@@ -253,7 +250,7 @@ class IndexerIndexRow extends Component {
|
||||
className={styles.externalLink}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
title={translate('Website')}
|
||||
to={baseUrl.replace('api.', '')}
|
||||
to={indexerUrls[0].replace('api.', '')}
|
||||
/> : null
|
||||
}
|
||||
|
||||
@@ -302,7 +299,6 @@ IndexerIndexRow.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
enable: PropTypes.bool.isRequired,
|
||||
redirect: PropTypes.bool.isRequired,
|
||||
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
appProfile: PropTypes.object.isRequired,
|
||||
status: PropTypes.object,
|
||||
capabilities: PropTypes.object,
|
||||
|
||||
@@ -20,14 +20,11 @@ function IndexerInfoModalContent(props) {
|
||||
encoding,
|
||||
language,
|
||||
indexerUrls,
|
||||
fields,
|
||||
protocol,
|
||||
capabilities,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? indexerUrls[0];
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
@@ -60,7 +57,7 @@ function IndexerInfoModalContent(props) {
|
||||
/>
|
||||
<DescriptionListItemTitle>{translate('IndexerSite')}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={baseUrl}>{baseUrl}</Link>
|
||||
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
@@ -117,7 +114,6 @@ IndexerInfoModalContent.propTypes = {
|
||||
encoding: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
capabilities: PropTypes.object.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
$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;
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
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;
|
||||
@@ -1,11 +0,0 @@
|
||||
.grid {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
&:hover {
|
||||
.content {
|
||||
background-color: var(--tableRowHoverBackgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
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;
|
||||
@@ -1,32 +0,0 @@
|
||||
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,6 +11,8 @@ 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';
|
||||
@@ -21,17 +23,12 @@ 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(isSmallScreen) {
|
||||
if (isSmallScreen) {
|
||||
return SearchIndexOverviewsConnector;
|
||||
}
|
||||
|
||||
function getViewComponent() {
|
||||
return SearchIndexTableConnector;
|
||||
}
|
||||
|
||||
@@ -47,6 +44,8 @@ class SearchIndex extends Component {
|
||||
scroller: null,
|
||||
jumpBarItems: { order: [] },
|
||||
jumpToCharacter: null,
|
||||
isAddIndexerModalOpen: false,
|
||||
isEditIndexerModalOpen: false,
|
||||
searchType: null,
|
||||
lastToggled: null,
|
||||
allSelected: false,
|
||||
@@ -178,6 +177,21 @@ 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 });
|
||||
};
|
||||
@@ -239,7 +253,6 @@ class SearchIndex extends Component {
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
onFilterSelect,
|
||||
isSmallScreen,
|
||||
hasIndexers,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -247,6 +260,8 @@ class SearchIndex extends Component {
|
||||
const {
|
||||
scroller,
|
||||
jumpBarItems,
|
||||
isAddIndexerModalOpen,
|
||||
isEditIndexerModalOpen,
|
||||
jumpToCharacter,
|
||||
selectedState,
|
||||
allSelected,
|
||||
@@ -255,7 +270,7 @@ class SearchIndex extends Component {
|
||||
|
||||
const selectedIndexerIds = this.getSelectedIds();
|
||||
|
||||
const ViewComponent = getViewComponent(isSmallScreen);
|
||||
const ViewComponent = getViewComponent();
|
||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||
const hasNoIndexer = !totalItems;
|
||||
|
||||
@@ -369,6 +384,16 @@ 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 130px;
|
||||
flex: 0 0 110px;
|
||||
}
|
||||
|
||||
.age,
|
||||
|
||||
@@ -18,20 +18,20 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createReleaseSelector(),
|
||||
(
|
||||
release
|
||||
movie
|
||||
) => {
|
||||
|
||||
// If a release is deleted this selector may fire before the parent
|
||||
// selecors, which will result in an undefined release, if that happens
|
||||
// If a movie is deleted this selector may fire before the parent
|
||||
// selecors, which will result in an undefined movie, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
// trying to show a release that has no information available.
|
||||
// trying to show a movie that has no information available.
|
||||
|
||||
if (!release) {
|
||||
if (!movie) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
...release
|
||||
...movie
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -41,7 +41,7 @@ const mapDispatchToProps = {
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class SearchIndexItemConnector extends Component {
|
||||
class MovieIndexItemConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -66,9 +66,9 @@ class SearchIndexItemConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
SearchIndexItemConnector.propTypes = {
|
||||
MovieIndexItemConnector.propTypes = {
|
||||
guid: PropTypes.string,
|
||||
component: PropTypes.elementType.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
.category {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 130px;
|
||||
flex: 0 0 110px;
|
||||
}
|
||||
|
||||
.age,
|
||||
|
||||
@@ -81,6 +81,8 @@ class EditDownloadClientModalContent extends Component {
|
||||
message
|
||||
} = item;
|
||||
|
||||
console.log(supportsCategories);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
|
||||
@@ -11,20 +11,12 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export const authenticationRequiredWarning = translate('AuthenticationRequiredWarning');
|
||||
|
||||
export const authenticationMethodOptions = [
|
||||
{ key: 'none', value: 'None', isDisabled: true },
|
||||
{ key: 'external', value: 'External', isHidden: true },
|
||||
const authenticationMethodOptions = [
|
||||
{ key: 'none', value: 'None' },
|
||||
{ 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' },
|
||||
@@ -76,7 +68,6 @@ class SecuritySettings extends Component {
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
apiKey,
|
||||
@@ -95,31 +86,13 @@ class SecuritySettings extends Component {
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={authenticationRequiredWarning}
|
||||
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 ?
|
||||
authenticationEnabled &&
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
@@ -129,12 +102,11 @@ class SecuritySettings extends Component {
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
authenticationEnabled &&
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
@@ -144,8 +116,7 @@ class SecuritySettings extends Component {
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
|
||||
@@ -14,6 +14,8 @@ function createLanguagesSelector() {
|
||||
return createSelector(
|
||||
(state) => state.localization,
|
||||
(localization) => {
|
||||
console.log(localization);
|
||||
|
||||
const items = localization.items;
|
||||
|
||||
if (!items) {
|
||||
|
||||
@@ -113,6 +113,8 @@ 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,6 +310,8 @@ 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" />
|
||||
<meta name="description" content="Prowlarr (Preview)" />
|
||||
|
||||
<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</title>
|
||||
<title>Prowlarr (Preview)</title>
|
||||
|
||||
<!--
|
||||
The super basic styling for .root will live here,
|
||||
|
||||
111
package.json
111
package.json
@@ -11,8 +11,7 @@
|
||||
"lint": "esprint check",
|
||||
"lint-fix": "esprint check --fix",
|
||||
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
|
||||
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc",
|
||||
"check-modules": "are-you-es5 check . -r"
|
||||
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
||||
},
|
||||
"repository": "https://github.com/Prowlarr/Prowlarr",
|
||||
"author": "Team Prowlarr",
|
||||
@@ -26,109 +25,107 @@
|
||||
"not chrome < 60"
|
||||
],
|
||||
"dependencies": {
|
||||
"@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.13",
|
||||
"@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",
|
||||
"@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",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "10.0.6",
|
||||
"filesize": "6.3.0",
|
||||
"history": "4.10.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.6.2",
|
||||
"jquery": "3.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.4",
|
||||
"moment": "2.29.2",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.10.3",
|
||||
"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.5.0",
|
||||
"react-custom-scrollbars-2": "4.4.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.9.2",
|
||||
"react-focus-lock": "2.5.0",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-measure": "1.4.7",
|
||||
"react-popper": "1.3.7",
|
||||
"react-redux": "8.0.5",
|
||||
"react-redux": "7.2.4",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-virtualized": "9.21.1",
|
||||
"redux": "4.2.0",
|
||||
"redux": "4.1.0",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.4.2",
|
||||
"reselect": "4.1.7"
|
||||
"redux-thunk": "2.3.0",
|
||||
"reselect": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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/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/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"are-you-es5": "2.1.2",
|
||||
"autoprefixer": "10.4.13",
|
||||
"babel-loader": "9.1.0",
|
||||
"@babel/preset-env": "7.18.2",
|
||||
"@babel/preset-react": "7.17.12",
|
||||
"autoprefixer": "10.4.7",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.26.1",
|
||||
"css-loader": "6.7.3",
|
||||
"eslint": "8.30.0",
|
||||
"core-js": "3.22.8",
|
||||
"css-loader": "6.7.1",
|
||||
"eslint": "8.17.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-plugin-simple-import-sort": "8.0.0",
|
||||
"eslint-plugin-react": "7.30.0",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"esprint": "3.6.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"filemanager-webpack-plugin": "6.1.7",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.2",
|
||||
"postcss": "8.4.20",
|
||||
"loader-utils": "^3.0.0",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"postcss": "8.4.14",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.0.2",
|
||||
"postcss-mixins": "9.0.4",
|
||||
"postcss-nested": "6.0.0",
|
||||
"postcss-simple-vars": "7.0.1",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-mixins": "9.0.2",
|
||||
"postcss-nested": "5.0.6",
|
||||
"postcss-simple-vars": "6.0.3",
|
||||
"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.16.0",
|
||||
"stylelint": "14.8.5",
|
||||
"stylelint-order": "5.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.75.0",
|
||||
"webpack-cli": "5.0.1",
|
||||
"webpack": "5.73.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"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.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<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(true);
|
||||
_runner.Start();
|
||||
|
||||
driver.Url = "http://localhost:9696";
|
||||
|
||||
|
||||
@@ -53,26 +53,6 @@ namespace NzbDrone.Common.Test.Http
|
||||
newUri.FullUri.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("", "./relative", "relative")]
|
||||
[TestCase("/", "./relative", "/relative")]
|
||||
[TestCase("/base", "./relative", "/relative")]
|
||||
[TestCase("/base/sub", "./relative", "/base/relative")]
|
||||
[TestCase("/base/sub/", "./relative", "/base/sub/relative")]
|
||||
[TestCase("base/sub", "./relative", "base/relative")]
|
||||
[TestCase("base/sub/", "./relative", "base/sub/relative")]
|
||||
[TestCase("", "../relative", "relative")]
|
||||
[TestCase("/", "../relative", "/relative")]
|
||||
[TestCase("/base", "../relative", "/relative")]
|
||||
[TestCase("/base/sub", "../relative", "/base/relative")]
|
||||
[TestCase("/base/sub/", "../relative", "/base/sub/relative")]
|
||||
[TestCase("base/sub", "../relative", "base/relative")]
|
||||
[TestCase("base/sub/", "../relative", "base/sub/relative")]
|
||||
public void should_combine_uri_with_dot_segment(string basePath, string relativePath, string expected)
|
||||
{
|
||||
var newUri = new HttpUri(basePath) + new HttpUri(relativePath);
|
||||
newUri.FullUri.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("", "", "")]
|
||||
[TestCase("/", "", "/")]
|
||||
[TestCase("base", "", "base")]
|
||||
|
||||
@@ -24,16 +24,12 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
|
||||
[TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")]
|
||||
[TestCase(@"https://torrentseeds.org/api/torrents/filter?api_token=2b51db35e1912ffc138825a12b9933d2&name=&sortField=created_at&sortDirection=desc&perPage=100&page=1")]
|
||||
[TestCase(@"https://beyond-hd.me/torrent/download/the-next-365-days-2022-2160p-nf-web-dl-dual-ddp-51-dovi-hdr-hevc-apex.225146.2b51db35e1912ffc138825a12b9933d2")]
|
||||
[TestCase(@"https://anthelion.me/api.php?api_key=2b51db35e1910123321025a12b9933d2&o=json&t=movie&q=&tmdb=&imdb=&cat=&limit=100&offset=0")]
|
||||
[TestCase(@"https://avistaz.to/api/v1/jackett/auth: username=mySecret&password=mySecret&pid=mySecret")]
|
||||
|
||||
// Indexer and Download Client Responses
|
||||
|
||||
// avistaz response
|
||||
[TestCase(@"""download"":""https://avistaz.to/rss/download/2b51db35e1910123321025a12b9933d2/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
||||
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
[TestCase(@"""token"":""2b51db35e1910123321025a12b9933d2""")]
|
||||
|
||||
// animebytes response
|
||||
[TestCase(@"""Link"":""https://animebytes.tv/torrent/994064/download/tb51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
@@ -76,7 +76,6 @@ namespace NzbDrone.Common.Http
|
||||
get
|
||||
{
|
||||
var newUrl = Headers["Location"];
|
||||
|
||||
if (newUrl == null)
|
||||
{
|
||||
newUrl = Headers["Refresh"];
|
||||
|
||||
@@ -166,37 +166,6 @@ namespace NzbDrone.Common.Http
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
if (relativePath.StartsWith("./"))
|
||||
{
|
||||
relativePath = relativePath.TrimStart('.').TrimStart('/');
|
||||
|
||||
var lastIndex = basePath.LastIndexOf("/");
|
||||
|
||||
if (lastIndex > 0)
|
||||
{
|
||||
basePath = basePath.Substring(0, lastIndex) + "/";
|
||||
}
|
||||
}
|
||||
|
||||
if (relativePath.StartsWith("../"))
|
||||
{
|
||||
relativePath = relativePath.TrimStart('.').TrimStart('/');
|
||||
|
||||
var lastIndex = basePath.LastIndexOf("/");
|
||||
|
||||
if (lastIndex > 0)
|
||||
{
|
||||
basePath = basePath.Substring(0, lastIndex) + "/";
|
||||
}
|
||||
|
||||
var secondLastIndex = basePath.LastIndexOf("/");
|
||||
|
||||
if (lastIndex > 0)
|
||||
{
|
||||
basePath = basePath.Substring(0, secondLastIndex) + "/";
|
||||
}
|
||||
}
|
||||
|
||||
var baseSlashIndex = basePath.LastIndexOf('/');
|
||||
|
||||
if (baseSlashIndex >= 0)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Common.Http
|
||||
{
|
||||
@@ -25,11 +25,5 @@ namespace NzbDrone.Common.Http
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TooManyRequestsException(HttpRequest request, HttpResponse response, TimeSpan retryWait)
|
||||
: base(request, response)
|
||||
{
|
||||
RetryAfter = retryWait;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -7,10 +8,10 @@ namespace NzbDrone.Common.Instrumentation
|
||||
{
|
||||
public class CleanseLogMessage
|
||||
{
|
||||
private static readonly Regex[] CleansingRules =
|
||||
{
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=[?&: ;])(apikey|api_key|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pid|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
@@ -20,7 +21,6 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?<secret>[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// UNIT3D
|
||||
new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
@@ -58,10 +58,9 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"""token"":""(?<secret>[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
};
|
||||
};
|
||||
|
||||
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
|
||||
|
||||
@@ -11,41 +11,26 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
try
|
||||
{
|
||||
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();
|
||||
}
|
||||
sentryEvent.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
|
||||
|
||||
if (sentryEvent.Fingerprint.Any())
|
||||
if (sentryEvent.Fingerprint != null)
|
||||
{
|
||||
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
|
||||
sentryEvent.SetFingerprint(fingerprint);
|
||||
}
|
||||
|
||||
if (sentryEvent.Extra.Any())
|
||||
if (sentryEvent.Extra != null)
|
||||
{
|
||||
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse(y.Value as string));
|
||||
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse((string)y.Value));
|
||||
sentryEvent.SetExtras(extras);
|
||||
}
|
||||
|
||||
if (sentryEvent.SentryExceptions is not null)
|
||||
foreach (var exception in sentryEvent.SentryExceptions)
|
||||
{
|
||||
foreach (var exception in sentryEvent.SentryExceptions)
|
||||
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
||||
foreach (var frame in exception.Stacktrace.Frames)
|
||||
{
|
||||
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
||||
if (exception.Stacktrace is not null)
|
||||
{
|
||||
foreach (var frame in exception.Stacktrace.Frames)
|
||||
{
|
||||
frame.FileName = ShortenPath(frame.FileName);
|
||||
}
|
||||
}
|
||||
frame.FileName = ShortenPath(frame.FileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Threading;
|
||||
using NLog;
|
||||
using NLog.Common;
|
||||
using NLog.Targets;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using Sentry;
|
||||
@@ -35,14 +34,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
SQLiteErrorCode.Auth
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> FilteredPostgresErrorCodes = new HashSet<string>
|
||||
{
|
||||
PostgresErrorCodes.OutOfMemory,
|
||||
PostgresErrorCodes.TooManyConnections,
|
||||
PostgresErrorCodes.DiskFull,
|
||||
PostgresErrorCodes.ProgramLimitExceeded
|
||||
};
|
||||
|
||||
// use string and not Type so we don't need a reference to the project
|
||||
// where these are defined
|
||||
private static readonly HashSet<string> FilteredExceptionTypeNames = new HashSet<string>
|
||||
@@ -51,7 +42,10 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
"UnauthorizedAccessException",
|
||||
|
||||
// Filter out people stuck in boot loops
|
||||
"CorruptDatabaseException"
|
||||
"CorruptDatabaseException",
|
||||
|
||||
// This also filters some people in boot loops
|
||||
"TinyIoCResolutionException"
|
||||
};
|
||||
|
||||
public static readonly List<string> FilteredExceptionMessages = new List<string>
|
||||
@@ -108,6 +102,9 @@ 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);
|
||||
@@ -213,11 +210,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
if (ex != null)
|
||||
{
|
||||
fingerPrint.Add(ex.GetType().FullName);
|
||||
if (ex.TargetSite != null)
|
||||
{
|
||||
fingerPrint.Add(ex.TargetSite.ToString());
|
||||
}
|
||||
|
||||
fingerPrint.Add(ex.TargetSite.ToString());
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
||||
@@ -248,19 +241,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
return false;
|
||||
}
|
||||
|
||||
var pgEx = logEvent.Exception as PostgresException;
|
||||
if (pgEx != null && FilteredPostgresErrorCodes.Contains(pgEx.SqlState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We don't care about transient network and timeout errors
|
||||
var npgEx = logEvent.Exception as NpgsqlException;
|
||||
if (npgEx != null && npgEx.IsTransient)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FilteredExceptionTypeNames.Contains(logEvent.Exception.GetType().Name))
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -4,19 +4,18 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<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="Npgsql" Version="5.0.11" />
|
||||
<PackageReference Include="Sentry" Version="3.24.1" />
|
||||
<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="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.1" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
<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,7 +18,6 @@
|
||||
<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"/>
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,55 +0,0 @@
|
||||
<?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("test");
|
||||
GivenValidBranch("master");
|
||||
|
||||
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);
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-15 04:26:21"));
|
||||
torrentInfo.Size.Should().Be(935127615);
|
||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -71,12 +71,12 @@ namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{{ .Today.Year }}")]
|
||||
public void should_handle_variables_statements(string template)
|
||||
[TestCase("{{ .Today.Year }}", "2022")]
|
||||
public void should_handle_variables_statements(string template, string expected)
|
||||
{
|
||||
var result = Subject.ApplyGoTemplateText(template, _variables);
|
||||
|
||||
result.Should().Be(DateTime.Now.Year.ToString());
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
|
||||
|
||||
@@ -8,7 +8,7 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Definitions.FileList;
|
||||
using NzbDrone.Core.Indexers.FileList;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -21,15 +21,10 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "FileList",
|
||||
Settings = new FileListSettings
|
||||
{
|
||||
BaseUrl = "https://filelist.io/",
|
||||
Username = "someuser",
|
||||
Passkey = "somepass"
|
||||
}
|
||||
Settings = new FileListSettings() { Username = "someuser", Passkey = "somepass" }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,9 +35,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
|
||||
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)));
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(4);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
@@ -55,14 +50,12 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 22:20:19"));
|
||||
torrentInfo.Size.Should().Be(8300512414);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
torrentInfo.Peers.Should().Be(2 + 12);
|
||||
torrentInfo.Seeders.Should().Be(12);
|
||||
|
||||
releases.Any(t => t.IndexerFlags.Contains(IndexerFlag.Internal)).Should().Be(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Definitions.FileList;
|
||||
using NzbDrone.Core.Indexers.FileList;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
@@ -16,35 +16,34 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Settings = new FileListSettings
|
||||
Subject.Settings = new FileListSettings()
|
||||
{
|
||||
BaseUrl = "https://filelist.io/",
|
||||
Passkey = "abcd",
|
||||
Username = "somename"
|
||||
Username = "somename",
|
||||
BaseUrl = "https://filelist.io"
|
||||
};
|
||||
|
||||
Subject.Capabilities = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.ImdbId, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
||||
},
|
||||
{
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
},
|
||||
{
|
||||
BookSearchParam.Q
|
||||
},
|
||||
Flags = new List<IndexerFlag>
|
||||
{
|
||||
IndexerFlag.FreeLeech,
|
||||
IndexerFlag.Internal,
|
||||
IndexerFlag.FreeLeech
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,7 +53,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
_movieSearchCriteria = new MovieSearchCriteria
|
||||
{
|
||||
SearchTerm = "Star Wars",
|
||||
Categories = new[] { 2000 }
|
||||
Categories = new int[] { 2000 }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,13 +65,13 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
[Test]
|
||||
public void should_use_categories_for_feed()
|
||||
{
|
||||
var results = Subject.GetSearchRequests(new MovieSearchCriteria { Categories = new[] { NewznabStandardCategory.MoviesSD.Id, NewznabStandardCategory.MoviesDVD.Id } });
|
||||
var results = Subject.GetSearchRequests(new MovieSearchCriteria { Categories = new int[] { NewznabStandardCategory.MoviesSD.Id, NewznabStandardCategory.MoviesDVD.Id } });
|
||||
|
||||
results.GetAllTiers().Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
|
||||
page.Url.Query.Should().Contain("&category=1%2C2");
|
||||
page.Url.Query.Should().Contain("&category=1,2&");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -101,7 +100,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
var page = results.GetAllTiers().First().First();
|
||||
|
||||
page.Url.Query.Should().Contain("type=name");
|
||||
page.Url.Query.Should().Contain("query=Star+Wars");
|
||||
page.Url.Query.Should().Contain("query=Star Wars");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
@@ -16,14 +15,14 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[TestFixture]
|
||||
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
|
||||
{
|
||||
private NewznabSettings _settings;
|
||||
private GenericNewznabSettings _settings;
|
||||
private IndexerDefinition _definition;
|
||||
private string _caps;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_settings = new NewznabSettings()
|
||||
_settings = new GenericNewznabSettings()
|
||||
{
|
||||
BaseUrl = "http://indxer.local"
|
||||
};
|
||||
@@ -71,45 +70,6 @@ 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<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_caps);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
|
||||
public class NewznabRequestGeneratorFixture : CoreTest<GenericNewznabRequestGenerator>
|
||||
{
|
||||
private MovieSearchCriteria _movieSearchCriteria;
|
||||
private TvSearchCriteria _tvSearchCriteria;
|
||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
Subject.Settings = new NewznabSettings()
|
||||
Subject.Settings = new GenericNewznabSettings()
|
||||
{
|
||||
BaseUrl = "http://127.0.0.1:1234/",
|
||||
ApiKey = "abcd",
|
||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
_capabilities = new IndexerCapabilities();
|
||||
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_capabilities);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Definitions;
|
||||
using NzbDrone.Core.Indexers.Definitions.Gazelle;
|
||||
using NzbDrone.Core.Indexers.Gazelle;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
@@ -24,12 +24,12 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "Orpheus",
|
||||
Settings = new OrpheusSettings { Apikey = "somekey" }
|
||||
Settings = new OrpheusSettings() { Apikey = "somekey" }
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_parse_recent_feed_from_Orpheus()
|
||||
public async Task should_parse_recent_feed_from_GazelleGames()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Orpheus/recentfeed.json");
|
||||
|
||||
@@ -37,14 +37,14 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
|
||||
.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[] { 2000 } })).Releases;
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(65);
|
||||
releases.First().Should().BeOfType<GazelleInfo>();
|
||||
|
||||
var torrentInfo = releases.First() as GazelleInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("The Beatles - Abbey Road [1969] [Album] [2.0 Mix 2019] [MP3 V2 (VBR)] [BD]");
|
||||
torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [MP3 V2 (VBR)] [BD]");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448");
|
||||
torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448");
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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.Definitions.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,12 +34,8 @@ 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<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||
.Returns(_caps);
|
||||
}
|
||||
|
||||
@@ -133,38 +129,6 @@ 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.3.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="12.0.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(1), 500);
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 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(5), 500);
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 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(1), 500);
|
||||
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("TODO No Updates On Server")]
|
||||
public void finds_update_when_version_lower()
|
||||
{
|
||||
UseRealHttp();
|
||||
|
||||
@@ -74,8 +74,6 @@ 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)
|
||||
@@ -147,7 +145,6 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
|
||||
Enabled = indexer.Enable,
|
||||
Type = schema,
|
||||
Priority = indexer.Priority
|
||||
};
|
||||
|
||||
return lazyLibrarianIndexer;
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
public bool Enabled { get; set; }
|
||||
public string Altername { get; set; }
|
||||
public LazyLibrarianProviderType Type { get; set; }
|
||||
public int Priority { get; set; }
|
||||
|
||||
public bool Equals(LazyLibrarianIndexer other)
|
||||
{
|
||||
@@ -44,8 +43,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
other.Name == Name &&
|
||||
other.Categories == Categories &&
|
||||
other.Enabled == Enabled &&
|
||||
other.Altername == Altername &&
|
||||
other.Priority == Priority;
|
||||
other.Altername == Altername;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
|
||||
public class LazyLibrarianV1Proxy : ILazyLibrarianV1Proxy
|
||||
{
|
||||
private const int ProwlarrHighestPriority = 50;
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -92,8 +90,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{ "host", indexer.Host },
|
||||
{ "prov_apikey", indexer.Apikey },
|
||||
{ "enabled", indexer.Enabled.ToString().ToLower() },
|
||||
{ "categories", indexer.Categories },
|
||||
{ "dlpriority", CalculatePriority(indexer.Priority).ToString() }
|
||||
{ "categories", indexer.Categories }
|
||||
};
|
||||
|
||||
var request = BuildRequest(settings, "/api", "addProvider", HttpMethod.Get, parameters);
|
||||
@@ -111,8 +108,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{ "prov_apikey", indexer.Apikey },
|
||||
{ "enabled", indexer.Enabled.ToString().ToLower() },
|
||||
{ "categories", indexer.Categories },
|
||||
{ "altername", indexer.Altername },
|
||||
{ "dlpriority", CalculatePriority(indexer.Priority).ToString() }
|
||||
{ "altername", indexer.Altername }
|
||||
};
|
||||
|
||||
var request = BuildRequest(settings, "/api", "changeProvider", HttpMethod.Get, parameters);
|
||||
@@ -195,7 +191,5 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private int CalculatePriority(int indexerPriority) => ProwlarrHighestPriority - indexerPriority + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +91,6 @@ 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)
|
||||
@@ -128,12 +126,6 @@ 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);
|
||||
}
|
||||
@@ -167,7 +159,6 @@ 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();
|
||||
@@ -184,11 +175,9 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = new List<LidarrField>()
|
||||
Fields = schema.Fields,
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -196,13 +185,13 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
if (indexer.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
|
||||
if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||
{
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ 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; }
|
||||
|
||||
@@ -34,7 +33,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.Equals(otherApiPath);
|
||||
var apiPathCompare = apiPath == 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,8 +75,6 @@ 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,8 +91,6 @@ 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)
|
||||
@@ -128,12 +126,6 @@ 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);
|
||||
}
|
||||
@@ -167,7 +159,6 @@ 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();
|
||||
@@ -184,11 +175,9 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = new List<RadarrField>()
|
||||
Fields = schema.Fields,
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -196,7 +185,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
if (indexer.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ 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; }
|
||||
|
||||
@@ -34,7 +33,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.Equals(otherApiPath);
|
||||
var apiPathCompare = apiPath == 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,8 +91,6 @@ 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)
|
||||
@@ -128,8 +126,6 @@ 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);
|
||||
}
|
||||
@@ -163,7 +159,6 @@ 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();
|
||||
@@ -180,11 +175,9 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = new List<ReadarrField>()
|
||||
Fields = schema.Fields,
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -192,13 +185,13 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
if (indexer.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
|
||||
if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
|
||||
{
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((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.Equals(otherApiPath);
|
||||
var apiPathCompare = apiPath == 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,8 +91,6 @@ 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)
|
||||
@@ -128,13 +126,6 @@ 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);
|
||||
}
|
||||
@@ -168,7 +159,6 @@ 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();
|
||||
@@ -185,11 +175,9 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = new List<SonarrField>()
|
||||
Fields = schema.Fields,
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -198,10 +186,10 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
if (indexer.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
|
||||
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.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
|
||||
return sonarrIndexer;
|
||||
|
||||
@@ -17,8 +17,6 @@ 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; }
|
||||
|
||||
@@ -36,7 +34,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.Equals(otherApiPath);
|
||||
var apiPathCompare = apiPath == 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,8 +91,6 @@ 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)
|
||||
@@ -128,8 +126,6 @@ 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);
|
||||
}
|
||||
@@ -163,7 +159,6 @@ 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();
|
||||
@@ -180,11 +175,9 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = new List<WhisparrField>()
|
||||
Fields = schema.Fields,
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -192,7 +185,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
|
||||
if (indexer.Protocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
|
||||
}
|
||||
|
||||
@@ -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.Equals(otherApiPath);
|
||||
var apiPathCompare = apiPath == 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);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
public enum AuthenticationRequiredType
|
||||
{
|
||||
Enabled = 0,
|
||||
DisabledForLocalAddresses = 1
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
namespace NzbDrone.Core.Authentication
|
||||
namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
public enum AuthenticationType
|
||||
{
|
||||
None = 0,
|
||||
Basic = 1,
|
||||
Forms = 2,
|
||||
External = 3
|
||||
Forms = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Authentication
|
||||
@@ -8,7 +8,5 @@ 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,6 +1,4 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -23,10 +21,6 @@ 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;
|
||||
@@ -36,15 +30,12 @@ namespace NzbDrone.Core.Authentication
|
||||
|
||||
public User Add(string username, string password)
|
||||
{
|
||||
var user = new User
|
||||
return _repo.Insert(new User
|
||||
{
|
||||
Identifier = Guid.NewGuid(),
|
||||
Username = username.ToLowerInvariant()
|
||||
};
|
||||
|
||||
SetUserHashedPassword(user, password);
|
||||
|
||||
return _repo.Insert(user);
|
||||
Username = username.ToLowerInvariant(),
|
||||
Password = password.SHA256Hash()
|
||||
});
|
||||
}
|
||||
|
||||
public User Update(User user)
|
||||
@@ -63,7 +54,7 @@ namespace NzbDrone.Core.Authentication
|
||||
|
||||
if (user.Password != password)
|
||||
{
|
||||
SetUserHashedPassword(user, password);
|
||||
user.Password = password.SHA256Hash();
|
||||
}
|
||||
|
||||
user.Username = username.ToLowerInvariant();
|
||||
@@ -90,20 +81,7 @@ namespace NzbDrone.Core.Authentication
|
||||
return null;
|
||||
}
|
||||
|
||||
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))
|
||||
if (user.Password == password.SHA256Hash())
|
||||
{
|
||||
return user;
|
||||
}
|
||||
@@ -115,42 +93,5 @@ 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,7 +32,6 @@ 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; }
|
||||
@@ -194,12 +193,10 @@ 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", "master").ToLowerInvariant();
|
||||
public string Branch => GetValue("Branch", "develop").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 = "master";
|
||||
DefaultBranch = "develop";
|
||||
|
||||
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
|
||||
{
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/NzbDrone.Core/Datastore/Migration/024_newznab_yml.cs
Normal file
14
src/NzbDrone.Core/Datastore/Migration/024_newznab_yml.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(24)]
|
||||
public class newznab_yml : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Update.Table("Indexers").Set(new { Implementation = "GenericNewznab", ConfigContract = "GenericNewznabSettings" }).Where(new { Implementation = "Newznab" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(025)]
|
||||
public class speedcd_userpasssettings_to_speedcdsettings : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "SpeedCDSettings" }).Where(new { Implementation = "SpeedCD" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(026)]
|
||||
public class torrentday_cookiesettings_to_torrentdaysettings : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "TorrentDaySettings" }).Where(new { Implementation = "TorrentDay" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration;
|
||||
|
||||
[Migration(027)]
|
||||
public class alpharatio_greatposterwall_config_contract : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "AlphaRatioSettings" }).Where(new { Implementation = "AlphaRatio" });
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "GreatPosterWallSettings" }).Where(new { Implementation = "GreatPosterWall" });
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration;
|
||||
|
||||
[Migration(028)]
|
||||
public class remove_notwhatcd : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
// Remove, site dead
|
||||
Delete.FromTable("Indexers").Row(new { Implementation = "NotWhatCD" });
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public class FreeboxDownloadException : DownloadClientException
|
||||
{
|
||||
public FreeboxDownloadException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public enum FreeboxDownloadPriority
|
||||
{
|
||||
Last = 0,
|
||||
First = 1
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user