Compare commits

..

18 Commits

Author SHA1 Message Date
Bogdan
a1927e1e0f Sort indexers by name in search footer dropdown 2023-07-29 13:48:10 +03:00
Bogdan
630a4ce800 Fixed: Ensure failing indexers are marked as failed when testing all
(cherry picked from commit b407eba61284d5fb855df6a2868805853aa6f448)
2023-07-29 12:14:58 +03:00
Bogdan
8b1dd78300 Fixed: (Apps) Ensure populated capabilities for Torznab/Newznab definitions 2023-07-29 12:08:48 +03:00
Bogdan
cab50b35aa Convert some selectors to Typescript 2023-07-29 03:14:47 +03:00
Weblate
eee1be784b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-07-28 12:55:09 +03:00
Bogdan
269dc5688b New: (IPTorrents) Add new base url 2023-07-27 12:18:49 +03:00
Weblate
9bed795c89 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-07-27 12:14:26 +03:00
Bogdan
3b5f151252 New: Set default names for providers in Add Modals 2023-07-27 02:58:07 +03:00
Weblate
b3a541c9ff Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dortlix <jeremy.boely@hotmail.fr>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aguillement <adrien.guillement@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-07-26 07:18:25 +03:00
Bogdan
bc90fa2d3f Add unit to history cleanup days option 2023-07-26 07:17:06 +03:00
Bogdan
4b0a896434 Fixed: (Cardigann) Improvements to automatic logins with captcha 2023-07-26 05:35:42 +03:00
Bogdan
6be0e08635 Convert Delete Indexer to Typescript 2023-07-25 05:50:45 +03:00
Bogdan
f618901048 Convert Indexer Stats to Typescript 2023-07-25 05:50:45 +03:00
Weblate
809ed940e6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: SHUAI.W <x@ousui.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-07-25 01:05:11 +03:00
Qstick
7b14c2ee66 Bump version to 1.8.0 2023-07-23 23:25:30 -05:00
Qstick
4528d03931 Update Sentry DSN 2023-07-23 21:49:21 -05:00
Bogdan
e0b30d34b1 New: (Apps) Add Go To Application in UI 2023-07-23 22:03:49 +03:00
Bogdan
8edf483e69 Bump version to 1.7.4 2023-07-23 07:09:59 +03:00
64 changed files with 735 additions and 709 deletions

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.7.3'
majorVersion: '1.8.0'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'

View File

@@ -26,7 +26,8 @@ module.exports = {
globals: {
expect: false,
chai: false,
sinon: false
sinon: false,
JSX: true
},
parserOptions: {

View File

@@ -5,7 +5,7 @@ import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import HistoryConnector from 'History/HistoryConnector';
import IndexerIndex from 'Indexer/Index/IndexerIndex';
import StatsConnector from 'Indexer/Stats/StatsConnector';
import IndexerStats from 'Indexer/Stats/IndexerStats';
import SearchIndexConnector from 'Search/SearchIndexConnector';
import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSettingsConnector';
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
@@ -60,7 +60,7 @@ function AppRoutes(props) {
<Route
path="/indexers/stats"
component={StatsConnector}
component={IndexerStats}
/>
{/*

View File

@@ -1,4 +1,9 @@
import IndexerAppState, { IndexerIndexAppState } from './IndexerAppState';
import CommandAppState from './CommandAppState';
import IndexerAppState, {
IndexerIndexAppState,
IndexerStatusAppState,
} from './IndexerAppState';
import IndexerStatsAppState from './IndexerStatsAppState';
import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState';
@@ -35,7 +40,10 @@ export interface CustomFilter {
}
interface AppState {
commands: CommandAppState;
indexerIndex: IndexerIndexAppState;
indexerStats: IndexerStatsAppState;
indexerStatus: IndexerStatusAppState;
indexers: IndexerAppState;
settings: SettingsAppState;
tags: TagsAppState;

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;

View File

@@ -1,6 +1,6 @@
import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
import Indexer from 'Indexer/Indexer';
import Indexer, { IndexerStatus } from 'Indexer/Indexer';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
@@ -30,4 +30,6 @@ interface IndexerAppState
AppSectionDeleteState,
AppSectionSaveState {}
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
export default IndexerAppState;

View File

@@ -0,0 +1,11 @@
import { AppSectionItemState } from 'App/State/AppSectionState';
import { Filter } from 'App/State/AppState';
import { IndexerStats } from 'typings/IndexerStats';
export interface IndexerStatsAppState
extends AppSectionItemState<IndexerStats> {
selectedFilterKey: string;
filters: Filter[];
}
export default IndexerStatsAppState;

View File

@@ -7,6 +7,11 @@ import DownloadClient from 'typings/DownloadClient';
import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings';
export interface AppProfileAppState
extends AppSectionState<Application>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface ApplicationAppState
extends AppSectionState<Application>,
AppSectionDeleteState,
@@ -24,6 +29,7 @@ export interface NotificationAppState
export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState {
appProfiles: AppProfileAppState;
applications: ApplicationAppState;
downloadClients: DownloadClientAppState;
notifications: NotificationAppState;

View File

@@ -0,0 +1,37 @@
import ModelBase from 'App/ModelBase';
export interface CommandBody {
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
completionMessage: string;
requiresDiskAccess: boolean;
isExclusive: boolean;
isLongRunning: boolean;
name: string;
lastExecutionTime: string;
lastStartTime: string;
trigger: string;
suppressMessages: boolean;
seriesId?: number;
}
interface Command extends ModelBase {
name: string;
commandName: string;
message: string;
body: CommandBody;
priority: string;
status: string;
result: string;
queued: string;
started: string;
ended: string;
duration: string;
trigger: string;
stateChangeTime: string;
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
lastExecutionTime: string;
}
export default Command;

View File

@@ -270,6 +270,7 @@ FormInputGroup.propTypes = {
helpTexts: PropTypes.arrayOf(PropTypes.string),
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
autoFocus: PropTypes.bool,
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object,

View File

@@ -3,13 +3,15 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import titleCase from 'Utilities/String/titleCase';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.indexers,
createSortedSectionSelector('indexers', sortByName),
(value, indexers) => {
const values = [];
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));

View File

@@ -68,6 +68,7 @@ class PathInputConnector extends Component {
}
PathInputConnector.propTypes = {
...PathInput.props,
includeFiles: PropTypes.bool.isRequired,
dispatchFetchPaths: PropTypes.func.isRequired,
dispatchClearPaths: PropTypes.func.isRequired

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'cell': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
import TableRowCell from './TableRowCell';
import styles from './TableRowCellButton.css';
function TableRowCellButton({ className, ...otherProps }) {
return (
<Link
className={className}
component={TableRowCell}
{...otherProps}
/>
);
}
TableRowCellButton.propTypes = {
className: PropTypes.string.isRequired
};
TableRowCellButton.defaultProps = {
className: styles.cell
};
export default TableRowCellButton;

View File

@@ -0,0 +1,19 @@
import React, { ReactNode } from 'react';
import Link, { LinkProps } from 'Components/Link/Link';
import TableRowCell from './TableRowCell';
import styles from './TableRowCellButton.css';
interface TableRowCellButtonProps extends LinkProps {
className?: string;
children: ReactNode;
}
function TableRowCellButton(props: TableRowCellButtonProps) {
const { className = styles.cell, ...otherProps } = props;
return (
<Link className={className} component={TableRowCell} {...otherProps} />
);
}
export default TableRowCellButton;

View File

@@ -62,6 +62,7 @@ class HistoryOptions extends Component {
<FormInputGroup
type={inputTypes.NUMBER}
name="historyCleanupDays"
unit={translate('days')}
value={historyCleanupDays}
helpText={translate('HistoryCleanupDaysHelpText')}
helpTextWarning={translate('HistoryCleanupDaysHelpTextWarning')}

View File

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

View File

@@ -0,0 +1,25 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
interface DeleteIndexerModalProps {
isOpen: boolean;
indexerId: number;
onModalClose(): void;
}
function DeleteIndexerModal(props: DeleteIndexerModalProps) {
const { isOpen, indexerId, onModalClose } = props;
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<DeleteIndexerModalContent
indexerId={indexerId}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default DeleteIndexerModal;

View File

@@ -1,88 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class DeleteIndexerModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false,
addImportExclusion: false
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
};
onAddImportExclusionChange = ({ value }) => {
this.setState({ addImportExclusion: value });
};
onDeleteMovieConfirmed = () => {
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = this.state.addImportExclusion;
this.setState({ deleteFiles: false, addImportExclusion: false });
this.props.onDeletePress(deleteFiles, addImportExclusion);
};
//
// Render
render() {
const {
name,
onModalClose
} = this.props;
return (
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
Delete - {name}
</ModalHeader>
<ModalBody>
{`Are you sure you want to delete ${name} from Prowlarr`}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteMovieConfirmed}
>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteIndexerModalContent.propTypes = {
name: PropTypes.string.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteIndexerModalContent;

View File

@@ -0,0 +1,51 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import { deleteIndexer } from 'Store/Actions/indexerActions';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import translate from 'Utilities/String/translate';
interface DeleteIndexerModalContentProps {
indexerId: number;
onModalClose(): void;
}
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
const { indexerId, onModalClose } = props;
const { name } = useSelector(createIndexerSelectorForHook(indexerId));
const dispatch = useDispatch();
const onConfirmDelete = useCallback(() => {
dispatch(deleteIndexer({ id: indexerId }));
onModalClose();
}, [indexerId, dispatch, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Delete')} - {name}
</ModalHeader>
<ModalBody>
{translate('AreYouSureYouWantToDeleteIndexer', [name])}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={onConfirmDelete}>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default DeleteIndexerModalContent;

View File

@@ -1,57 +0,0 @@
import { push } from 'connected-react-router';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteIndexer } from 'Store/Actions/indexerActions';
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
function createMapStateToProps() {
return createSelector(
createIndexerSelector(),
(indexer) => {
return indexer;
}
);
}
const mapDispatchToProps = {
deleteIndexer,
push
};
class DeleteIndexerModalContentConnector extends Component {
//
// Listeners
onDeletePress = () => {
this.props.deleteIndexer({
id: this.props.indexerId
});
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<DeleteIndexerModalContent
{...this.props}
onDeletePress={this.onDeletePress}
/>
);
}
}
DeleteIndexerModalContentConnector.propTypes = {
indexerId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired,
deleteIndexer: PropTypes.func.isRequired,
push: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(DeleteIndexerModalContentConnector);

View File

@@ -12,7 +12,7 @@ interface IndexerStatusCellProps {
className: string;
enabled: boolean;
redirect: boolean;
status: IndexerStatus;
status?: IndexerStatus;
longDateFormat: string;
timeFormat: string;
component?: React.ElementType;

View File

@@ -40,6 +40,10 @@ interface Indexer extends ModelBase {
added: Date;
enable: boolean;
redirect: boolean;
supportsRss: boolean;
supportsSearch: boolean;
supportsRedirect: boolean;
supportsPagination: boolean;
protocol: string;
privacy: string;
priority: number;
@@ -49,6 +53,7 @@ interface Indexer extends ModelBase {
status: IndexerStatus;
capabilities: IndexerCapabilities;
indexerUrls: string[];
legacyUrls: string[];
}
export default Indexer;

View File

@@ -1,15 +1,16 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './NoIndexer.css';
function NoIndexer(props) {
const {
totalItems,
onAddIndexerPress
} = props;
interface NoIndexerProps {
totalItems: number;
onAddIndexerPress(): void;
}
function NoIndexer(props: NoIndexerProps) {
const { totalItems, onAddIndexerPress } = props;
if (totalItems > 0) {
return (
@@ -28,10 +29,7 @@ function NoIndexer(props) {
</div>
<div className={styles.buttonContainer}>
<Button
onPress={onAddIndexerPress}
kind={kinds.PRIMARY}
>
<Button onPress={onAddIndexerPress} kind={kinds.PRIMARY}>
{translate('AddNewIndexer')}
</Button>
</div>
@@ -39,9 +37,4 @@ function NoIndexer(props) {
);
}
NoIndexer.propTypes = {
totalItems: PropTypes.number.isRequired,
onAddIndexerPress: PropTypes.func.isRequired
};
export default NoIndexer;

View File

@@ -0,0 +1,275 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import IndexerStatsAppState from 'App/State/IndexerStatsAppState';
import Alert from 'Components/Alert';
import BarChart from 'Components/Chart/BarChart';
import DoughnutChart from 'Components/Chart/DoughnutChart';
import StackedBarChart from 'Components/Chart/StackedBarChart';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import { align, kinds } from 'Helpers/Props';
import {
fetchIndexerStats,
setIndexerStatsFilter,
} from 'Store/Actions/indexerStatsActions';
import {
IndexerStatsHost,
IndexerStatsIndexer,
IndexerStatsUserAgent,
} from 'typings/IndexerStats';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import IndexerStatsFilterMenu from './IndexerStatsFilterMenu';
import styles from './IndexerStats.css';
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.averageResponseTime,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value:
(indexer.numberOfFailedQueries +
indexer.numberOfFailedRssQueries +
indexer.numberOfFailedAuthQueries +
indexer.numberOfFailedGrabs) /
(indexer.numberOfQueries +
indexer.numberOfRssQueries +
indexer.numberOfAuthQueries +
indexer.numberOfGrabs),
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) {
const data = {
labels: indexerStats.map((indexer) => indexer.indexerName),
datasets: [
{
label: translate('SearchQueries'),
data: indexerStats.map((indexer) => indexer.numberOfQueries),
},
{
label: translate('RssQueries'),
data: indexerStats.map((indexer) => indexer.numberOfRssQueries),
},
{
label: translate('AuthQueries'),
data: indexerStats.map((indexer) => indexer.numberOfAuthQueries),
},
],
};
return data;
}
function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfGrabs,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfQueries,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostGrabsData(indexerStats: IndexerStatsHost[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfGrabs,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostQueryData(indexerStats: IndexerStatsHost[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfQueries,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
const indexerStatsSelector = () => {
return createSelector(
(state: AppState) => state.indexerStats,
(indexerStats: IndexerStatsAppState) => {
return indexerStats;
}
);
};
function IndexerStats() {
const { isFetching, isPopulated, item, error, filters, selectedFilterKey } =
useSelector(indexerStatsSelector());
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchIndexerStats());
}, [dispatch]);
const onFilterSelect = useCallback(
(value: string) => {
dispatch(setIndexerStatsFilter({ selectedFilterKey: value }));
},
[dispatch]
);
const isLoaded = !error && isPopulated;
return (
<PageContent>
<PageToolbar>
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}>
<IndexerStatsFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
onFilterSelect={onFilterSelect}
isDisabled={false}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated && <LoadingIndicator />}
{!isFetching && !!error && (
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load indexer stats from API')}
</Alert>
)}
{isLoaded && (
<div>
<div className={styles.fullWidthChart}>
<BarChart
data={getAverageResponseTimeData(item.indexers)}
title={translate('AverageResponseTimesMs')}
/>
</div>
<div className={styles.fullWidthChart}>
<BarChart
data={getFailureRateData(item.indexers)}
title={translate('IndexerFailureRate')}
kind={kinds.WARNING}
/>
</div>
<div className={styles.halfWidthChart}>
<StackedBarChart
data={getTotalRequestsData(item.indexers)}
title={translate('TotalIndexerQueries')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getNumberGrabsData(item.indexers)}
title={translate('TotalIndexerSuccessfulGrabs')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentQueryData(item.userAgents)}
title={translate('TotalUserAgentQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentGrabsData(item.userAgents)}
title={translate('TotalUserAgentGrabs')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostQueryData(item.hosts)}
title={translate('TotalHostQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostGrabsData(item.hosts)}
title={translate('TotalHostGrabs')}
horizontal={true}
/>
</div>
</div>
)}
</PageContentBody>
</PageContent>
);
}
export default IndexerStats;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
interface IndexerStatsFilterMenuProps {
selectedFilterKey: string | number;
filters: object[];
isDisabled: boolean;
onFilterSelect(filterName: string): unknown;
}
function IndexerStatsFilterMenu(props: IndexerStatsFilterMenuProps) {
const { selectedFilterKey, filters, isDisabled, onFilterSelect } = props;
return (
<FilterMenu
alignMenu={align.RIGHT}
isDisabled={isDisabled}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
);
}
export default IndexerStatsFilterMenu;

View File

@@ -1,261 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import BarChart from 'Components/Chart/BarChart';
import DoughnutChart from 'Components/Chart/DoughnutChart';
import StackedBarChart from 'Components/Chart/StackedBarChart';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import { align, kinds } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import StatsFilterMenu from './StatsFilterMenu';
import styles from './Stats.css';
function getAverageResponseTimeData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.averageResponseTime
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getFailureRateData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: (indexer.numberOfFailedQueries + indexer.numberOfFailedRssQueries + indexer.numberOfFailedAuthQueries + indexer.numberOfFailedGrabs) /
(indexer.numberOfQueries + indexer.numberOfRssQueries + indexer.numberOfAuthQueries + indexer.numberOfGrabs)
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getTotalRequestsData(indexerStats) {
const data = {
labels: indexerStats.map((indexer) => indexer.indexerName),
datasets: [
{
label: 'Search Queries',
data: indexerStats.map((indexer) => indexer.numberOfQueries)
},
{
label: 'Rss Queries',
data: indexerStats.map((indexer) => indexer.numberOfRssQueries)
},
{
label: 'Auth Queries',
data: indexerStats.map((indexer) => indexer.numberOfAuthQueries)
}
]
};
return data;
}
function getNumberGrabsData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentGrabsData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfGrabs
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentQueryData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfQueries
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostGrabsData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfGrabs
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostQueryData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfQueries
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function Stats(props) {
const {
item,
isFetching,
isPopulated,
error,
filters,
selectedFilterKey,
onFilterSelect
} = props;
const isLoaded = !!(!error && isPopulated);
return (
<PageContent>
<PageToolbar>
<PageToolbarSection
alignContent={align.RIGHT}
collapseButtons={false}
>
<StatsFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
onFilterSelect={onFilterSelect}
isDisabled={false}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load indexer stats from API')}
</Alert>
}
{
isLoaded &&
<div>
<div className={styles.fullWidthChart}>
<BarChart
data={getAverageResponseTimeData(item.indexers)}
title={translate('AverageResponseTimesMs')}
/>
</div>
<div className={styles.fullWidthChart}>
<BarChart
data={getFailureRateData(item.indexers)}
title={translate('IndexerFailureRate')}
kind={kinds.WARNING}
/>
</div>
<div className={styles.halfWidthChart}>
<StackedBarChart
data={getTotalRequestsData(item.indexers)}
title={translate('TotalIndexerQueries')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getNumberGrabsData(item.indexers)}
title={translate('TotalIndexerSuccessfulGrabs')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentQueryData(item.userAgents)}
title={translate('TotalUserAgentQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentGrabsData(item.userAgents)}
title={translate('TotalUserAgentGrabs')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostQueryData(item.hosts)}
title={translate('TotalHostQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostGrabsData(item.hosts)}
title={translate('TotalHostGrabs')}
horizontal={true}
/>
</div>
</div>
}
</PageContentBody>
</PageContent>
);
}
Stats.propTypes = {
item: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired,
onFilterSelect: PropTypes.func.isRequired,
error: PropTypes.object,
data: PropTypes.object
};
export default Stats;

View File

@@ -1,51 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerStats, setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
import Stats from './Stats';
function createMapStateToProps() {
return createSelector(
(state) => state.indexerStats,
(indexerStats) => indexerStats
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onFilterSelect(selectedFilterKey) {
dispatch(setIndexerStatsFilter({ selectedFilterKey }));
},
dispatchFetchIndexerStats() {
dispatch(fetchIndexerStats());
}
};
}
class StatsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchIndexerStats();
}
//
// Render
render() {
return (
<Stats
{...this.props}
/>
);
}
}
StatsConnector.propTypes = {
dispatchFetchIndexerStats: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(StatsConnector);

View File

@@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
function StatsFilterMenu(props) {
const {
selectedFilterKey,
filters,
isDisabled,
onFilterSelect
} = props;
return (
<FilterMenu
alignMenu={align.RIGHT}
isDisabled={isDisabled}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
);
}
StatsFilterMenu.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
StatsFilterMenu.defaultProps = {
showCustomFilters: false
};
export default StatsFilterMenu;

View File

@@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
function createMapStateToProps() {
return createSelector(
(state) => state.indexerStats.items,
(state) => state.indexerStats.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'indexerStats'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setIndexerStatsFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);

View File

@@ -4,6 +4,11 @@
width: 290px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
@@ -12,6 +17,12 @@
font-size: 24px;
}
.externalLink {
composes: button from '~Components/Link/IconButton.css';
height: 36px;
}
.enabled {
display: flex;
flex-wrap: wrap;

View File

@@ -3,7 +3,9 @@
interface CssExports {
'application': string;
'enabled': string;
'externalLink': string;
'name': string;
'nameContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -2,9 +2,10 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditApplicationModalConnector from './EditApplicationModalConnector';
import styles from './Application.css';
@@ -57,18 +58,33 @@ class Application extends Component {
id,
name,
syncLevel,
fields,
tags,
tagList
} = this.props;
const applicationUrl = fields.find((field) => field.name === 'baseUrl')?.value;
return (
<Card
className={styles.application}
overlayContent={true}
onPress={this.onEditApplicationPress}
>
<div className={styles.name}>
{name}
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
{
applicationUrl ?
<IconButton
className={styles.externalLink}
name={icons.EXTERNAL_LINK}
title={translate('GoToApplication')}
to={`${applicationUrl}`}
/> : null
}
</div>
{
@@ -125,6 +141,7 @@ Application.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
syncLevel: PropTypes.string.isRequired,
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteApplication: PropTypes.func

View File

@@ -117,10 +117,7 @@ export default {
[SELECT_APPLICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.onGrab = selectedSchema.supportsOnGrab;
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
selectedSchema.onRename = selectedSchema.supportsOnRename;
selectedSchema.name = selectedSchema.implementationName;
return selectedSchema;
});

View File

@@ -142,6 +142,7 @@ export default {
[SELECT_DOWNLOAD_CLIENT_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.name = selectedSchema.implementationName;
selectedSchema.enable = true;
return selectedSchema;

View File

@@ -104,6 +104,8 @@ export default {
[SELECT_INDEXER_PROXY_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.name = selectedSchema.implementationName;
return selectedSchema;
});
}

View File

@@ -104,6 +104,7 @@ export default {
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.name = selectedSchema.implementationName;
selectedSchema.onGrab = selectedSchema.supportsOnGrab;
selectedSchema.onApplicationUpdate = selectedSchema.supportsOnApplicationUpdate;

View File

@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createAllIndexersSelector() {
return createSelector(
(state) => state.indexers,
(state: AppState) => state.indexers,
(indexers) => {
return indexers.items;
}

View File

@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createCommandsSelector() {
return createSelector(
(state) => state.commands,
(state: AppState) => state.commands,
(commands) => {
return commands.items;
}

View File

@@ -1,9 +1,10 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { isCommandExecuting } from 'Utilities/Command';
function createExecutingCommandsSelector() {
return createSelector(
(state) => state.commands.items,
(state: AppState) => state.commands.items,
(commands) => {
return commands.filter((command) => isCommandExecuting(command));
}

View File

@@ -1,9 +1,10 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { createIndexerSelectorForHook } from './createIndexerSelector';
function createIndexerAppProfileSelector(indexerId) {
function createIndexerAppProfileSelector(indexerId: number) {
return createSelector(
(state) => state.settings.appProfiles.items,
(state: AppState) => state.settings.appProfiles.items,
createIndexerSelectorForHook(indexerId),
(appProfiles, indexer = {}) => {
return appProfiles.find((profile) => {

View File

@@ -1,13 +0,0 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
function createIndexerStatusSelector(indexerId) {
return createSelector(
(state) => state.indexerStatus.items,
(indexerStatus) => {
return _.find(indexerStatus, { indexerId });
}
);
}
export default createIndexerStatusSelector;

View File

@@ -0,0 +1,15 @@
import { find } from 'lodash';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { IndexerStatus } from 'Indexer/Indexer';
function createIndexerStatusSelector(indexerId: number) {
return createSelector(
(state: AppState) => state.indexerStatus.items,
(indexerStatus) => {
return find(indexerStatus, { indexerId }) as IndexerStatus | undefined;
}
);
}
export default createIndexerStatusSelector;

View File

@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createTagsSelector() {
return createSelector(
(state) => state.tags.items,
(state: AppState) => state.tags.items,
(tags) => {
return tags;
}

View File

@@ -0,0 +1,31 @@
export interface IndexerStatsIndexer {
indexerId: number;
indexerName: string;
averageResponseTime: number;
numberOfQueries: number;
numberOfGrabs: number;
numberOfRssQueries: number;
numberOfAuthQueries: number;
numberOfFailedQueries: number;
numberOfFailedGrabs: number;
numberOfFailedRssQueries: number;
numberOfFailedAuthQueries: number;
}
export interface IndexerStatsUserAgent {
userAgent: string;
numberOfQueries: number;
numberOfGrabs: number;
}
export interface IndexerStatsHost {
host: string;
numberOfQueries: number;
numberOfGrabs: number;
}
export interface IndexerStats {
indexers: IndexerStatsIndexer[];
userAgents: IndexerStatsUserAgent[];
hosts: IndexerStatsHost[];
}

View File

@@ -13,6 +13,15 @@ export interface Field {
interface Notification extends ModelBase {
enable: boolean;
name: string;
onGrab: boolean;
onHealthIssue: boolean;
onHealthRestored: boolean;
includeHealthWarnings: boolean;
onApplicationUpdate: boolean;
supportsOnGrab: boolean;
supportsOnHealthIssue: boolean;
supportsOnHealthRestored: boolean;
supportsOnApplicationUpdate: boolean;
fields: Field[];
implementationName: string;
implementation: string;

View File

@@ -73,7 +73,7 @@ namespace NzbDrone.Common.Instrumentation
else
{
dsn = RuntimeInfo.IsProduction
? "https://d62a0313c35f4afc932b4a20e1072793@sentry.servarr.com/27"
? "https://a1fa00bd1d60465ebd9aca58c5a22d00@sentry.servarr.com/27"
: "https://e38306161ff945999adf774a16e933c3@sentry.servarr.com/30";
}

View File

@@ -46,7 +46,6 @@ namespace NzbDrone.Core.Applications
yield return new ApplicationDefinition
{
Name = GetType().Name,
SyncLevel = ApplicationSyncLevel.FullSync,
Implementation = GetType().Name,
Settings = config

View File

@@ -67,10 +67,11 @@ namespace NzbDrone.Core.Applications
public void HandleAsync(ProviderAddedEvent<IIndexer> message)
{
var enabledApps = _applicationsFactory.SyncEnabled();
var indexer = _indexerFactory.GetInstance((IndexerDefinition)message.Definition);
foreach (var app in enabledApps)
{
if (ShouldHandleIndexer(app.Definition, message.Definition))
if (ShouldHandleIndexer(app.Definition, indexer.Definition))
{
ExecuteAction(a => a.AddIndexer((IndexerDefinition)message.Definition), app);
}
@@ -92,8 +93,9 @@ namespace NzbDrone.Core.Applications
var enabledApps = _applicationsFactory.SyncEnabled()
.Where(n => ((ApplicationDefinition)n.Definition).SyncLevel == ApplicationSyncLevel.FullSync)
.ToList();
var indexer = _indexerFactory.GetInstance((IndexerDefinition)message.Definition);
SyncIndexers(enabledApps, new List<IndexerDefinition> { (IndexerDefinition)message.Definition });
SyncIndexers(enabledApps, new List<IndexerDefinition> { (IndexerDefinition)indexer.Definition });
}
public void HandleAsync(ApiKeyChangedEvent message)

View File

@@ -27,20 +27,7 @@ namespace NzbDrone.Core.Download
public virtual ProviderMessage Message => null;
public IEnumerable<ProviderDefinition> DefaultDefinitions
{
get
{
var config = (IProviderConfig)new TSettings();
yield return new DownloadClientDefinition
{
Name = GetType().Name,
Implementation = GetType().Name,
Settings = config
};
}
}
public IEnumerable<ProviderDefinition> DefaultDefinitions => new List<ProviderDefinition>();
public ProviderDefinition Definition { get; set; }

View File

@@ -14,20 +14,7 @@ namespace NzbDrone.Core.IndexerProxies
public Type ConfigContract => typeof(TSettings);
public IEnumerable<ProviderDefinition> DefaultDefinitions
{
get
{
var config = (IProviderConfig)new TSettings();
yield return new IndexerProxyDefinition
{
Name = GetType().Name,
Implementation = GetType().Name,
Settings = config
};
}
}
public IEnumerable<ProviderDefinition> DefaultDefinitions => new List<ProviderDefinition>();
public ProviderDefinition Definition { get; set; }
public abstract ValidationResult Test();

View File

@@ -266,16 +266,21 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
await GetConfigurationForSetup(true);
}
if (landingResultDocument == null)
{
throw new CardigannConfigException(_definition, $"Login failed: Invalid document received from {loginUrl}");
}
var form = landingResultDocument.QuerySelector(formSelector);
if (form == null)
{
throw new CardigannConfigException(_definition, string.Format("Login failed: No form found on {0} using form selector {1}", loginUrl, formSelector));
throw new CardigannConfigException(_definition, $"Login failed: No form found on {loginUrl} using form selector {formSelector}");
}
var inputs = form.QuerySelectorAll("input");
if (inputs == null)
{
throw new CardigannConfigException(_definition, string.Format("Login failed: No inputs found on {0} using form selector {1}", loginUrl, formSelector));
throw new CardigannConfigException(_definition, $"Login failed: No inputs found on {loginUrl} using form selector {formSelector}");
}
var submitUrlstr = form.GetAttribute("action");
@@ -616,7 +621,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
{
var login = _definition.Login;
if (login == null || login.Method != "form")
if (login is not { Method: "form" })
{
return null;
}
@@ -667,7 +672,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
if (captcha != null && automaticLogin)
{
_logger.Error("CardigannIndexer ({0}): Found captcha during automatic login, aborting", _definition.Id);
throw new CardigannConfigException(_definition, "Found captcha during automatic login, aborting");
}
return captcha;

View File

@@ -35,7 +35,8 @@ namespace NzbDrone.Core.Indexers.Definitions
"https://ipt.workisboring.net/",
"https://ipt.lol/",
"https://ipt.cool/",
"https://ipt.world/"
"https://ipt.world/",
"https://ipt.octopus.town/"
};
public override string Description => "IPTorrents (IPT) is a Private Torrent Tracker for 0DAY / GENERAL.";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public override bool SupportsPagination => true;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities { get => GetCapabilitiesFromSettings(); protected set => base.Capabilities = value; }
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings, Definition).LimitsDefault.Value;
public override int PageSize => GetProviderPageSize();
public override IIndexerRequestGenerator GetRequestGenerator()
{
@@ -43,14 +43,12 @@ namespace NzbDrone.Core.Indexers.Newznab
public string[] GetBaseUrlFromSettings()
{
var baseUrl = "";
if (Definition == null || Settings == null || Settings.Categories == null)
if (Definition == null || Settings?.Categories == null)
{
return new string[] { baseUrl };
return new[] { "" };
}
return new string[] { Settings.BaseUrl };
return new[] { Settings.BaseUrl };
}
protected override NewznabSettings GetDefaultBaseUrl(NewznabSettings settings)
@@ -62,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
var caps = new IndexerCapabilities();
if (Definition == null || Settings == null || Settings.Categories == null)
if (Definition == null || Settings?.Categories == null)
{
return caps;
}
@@ -211,5 +209,17 @@ namespace NzbDrone.Core.Indexers.Newznab
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details");
}
}
private int GetProviderPageSize()
{
try
{
return _capabilitiesProvider.GetCapabilities(Settings, Definition).LimitsDefault.GetValueOrDefault(100);
}
catch
{
return 100;
}
}
}
}

View File

@@ -34,9 +34,8 @@ namespace NzbDrone.Core.Indexers.Newznab
public IndexerCapabilities GetCapabilities(NewznabSettings indexerSettings, ProviderDefinition definition)
{
var key = indexerSettings.ToJson();
var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings, definition), TimeSpan.FromDays(7));
return capabilities;
return _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings, definition), TimeSpan.FromDays(7));
}
private IndexerCapabilities FetchCapabilities(NewznabSettings indexerSettings, ProviderDefinition definition)

View File

@@ -23,11 +23,9 @@ namespace NzbDrone.Core.Indexers.Torznab
public override bool FollowRedirect => true;
public override bool SupportsRedirect => true;
public override bool SupportsPagination => true;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings, Definition).LimitsDefault.Value;
public override IndexerCapabilities Capabilities { get => GetCapabilitiesFromSettings(); protected set => base.Capabilities = value; }
public override int PageSize => GetProviderPageSize();
public override IIndexerRequestGenerator GetRequestGenerator()
{
@@ -45,14 +43,12 @@ namespace NzbDrone.Core.Indexers.Torznab
public string[] GetBaseUrlFromSettings()
{
var baseUrl = "";
if (Definition == null || Settings == null || Settings.Categories == null)
if (Definition == null || Settings?.Categories == null)
{
return new string[] { baseUrl };
return new[] { "" };
}
return new string[] { Settings.BaseUrl };
return new[] { Settings.BaseUrl };
}
protected override TorznabSettings GetDefaultBaseUrl(TorznabSettings settings)
@@ -64,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Torznab
{
var caps = new IndexerCapabilities();
if (Definition == null || Settings == null || Settings.Categories == null)
if (Definition == null || Settings?.Categories == null)
{
return caps;
}
@@ -192,5 +188,17 @@ namespace NzbDrone.Core.Indexers.Torznab
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details");
}
}
private int GetProviderPageSize()
{
try
{
return _capabilitiesProvider.GetCapabilities(Settings, Definition).LimitsDefault.GetValueOrDefault(100);
}
catch
{
return 100;
}
}
}
}

View File

@@ -256,10 +256,19 @@ namespace NzbDrone.Core.Indexers
{
var result = base.Test(definition);
if ((result == null || result.IsValid) && definition.Id != 0)
if (definition.Id == 0)
{
return result;
}
if (result == null || result.IsValid)
{
_indexerStatusService.RecordSuccess(definition.Id);
}
else
{
_indexerStatusService.RecordFailure(definition.Id);
}
return result;
}

View File

@@ -52,10 +52,12 @@
"AppsMinimumSeeders": "Apps Minimum Seeders",
"AppsMinimumSeedersHelpText": "Minimum seeders required by the Applications for the indexer to grab, empty is Sync profile's default",
"AreYouSureYouWantToDeleteCategory": "Are you sure you want to delete mapped category?",
"AreYouSureYouWantToDeleteIndexer": "Are you sure you want to delete '{0}' from Prowlarr?",
"AreYouSureYouWantToResetYourAPIKey": "Are you sure you want to reset your API Key?",
"Artist": "Artist",
"AudioSearch": "Audio Search",
"Auth": "Auth",
"AuthQueries": "Auth Queries",
"Authentication": "Authentication",
"AuthenticationMethodHelpText": "Require Username and Password to access Prowlarr",
"AuthenticationRequired": "Authentication Required",
@@ -199,6 +201,7 @@
"GeneralSettings": "General Settings",
"GeneralSettingsSummary": "Port, SSL, username/password, proxy, analytics, and updates",
"Genre": "Genre",
"GoToApplication": "Go to application",
"GrabReleases": "Grab Release(s)",
"GrabTitle": "Grab Title",
"Grabbed": "Grabbed",
@@ -397,6 +400,7 @@
"Result": "Result",
"Retention": "Retention",
"RssFeed": "RSS Feed",
"RssQueries": "RSS Queries",
"SSLCertPassword": "SSL Cert Password",
"SSLCertPasswordHelpText": "Password for pfx file",
"SSLCertPath": "SSL Cert Path",
@@ -412,6 +416,7 @@
"SearchCapabilities": "Search Capabilities",
"SearchCountIndexers": "Search {0} indexers",
"SearchIndexers": "Search Indexers",
"SearchQueries": "Search Queries",
"SearchType": "Search Type",
"SearchTypes": "Search Types",
"Season": "Season",
@@ -551,5 +556,7 @@
"Year": "Year",
"Yes": "Yes",
"YesCancel": "Yes, Cancel",
"Yesterday": "Yesterday"
"Yesterday": "Yesterday",
"days": "days",
"minutes": "minutes"
}

View File

@@ -283,9 +283,9 @@
"MIA": "MIA",
"LaunchBrowserHelpText": " Ouvrer un navigateur Web et accéder à la page d'accueil de Prowlarr au démarrage de l'application.",
"CloseCurrentModal": "Fermer le modal actuel",
"AddingTag": "Ajouter un tag",
"AddingTag": "Ajout d'un tag",
"OnHealthIssueHelpText": "Sur un problème de santé",
"AcceptConfirmationModal": "Accepter la modalité de confirmation",
"AcceptConfirmationModal": "Accepter la fenêtre de confirmation",
"OpenThisModal": "Ouvrer ce modal",
"IndexerLongTermStatusCheckSingleClientMessage": "Indexeurs indisponibles en raison de pannes pendant plus de 6 heures : {0}",
"IndexerLongTermStatusCheckAllClientMessage": "Tous les indexeurs sont indisponibles en raison d'échecs de plus de 6 heures",

View File

@@ -551,5 +551,12 @@
"SeedTimeHelpText": "O tempo que um torrent deve ser propagado antes de parar, vazio é o padrão do aplicativo",
"SelectedCountOfCountReleases": "{0} de {1} lançamentos selecionados",
"NewznabUrl": "Url Newznab",
"TorznabUrl": "Url Torznab"
"TorznabUrl": "Url Torznab",
"GoToApplication": "Ir para o aplicativo",
"AreYouSureYouWantToDeleteIndexer": "Tem certeza de que deseja excluir '{0}' do Prowlarr?",
"AuthQueries": "Consultas de autenticação",
"RssQueries": "Consultas RSS",
"SearchQueries": "Consultas de pesquisa",
"minutes": "minutos",
"days": "dias"
}

View File

@@ -11,8 +11,8 @@
"Connections": "Conexiuni",
"Connect": "Conectează",
"Clear": "Șterge",
"BackupNow": "Fă o copie de siguranță",
"Backup": "Copie de siguranță",
"BackupNow": "Fă o copie de rezervă",
"Backup": "Copie de rezervă",
"AppDataLocationHealthCheckMessage": "Pentru a preveni ștergerea AppData, update-ul nu este posibil",
"Analytics": "Statistici",
"All": "Toate",
@@ -47,7 +47,7 @@
"NoChanges": "Nicio Modificare",
"NoChange": "Nicio Modificare",
"Name": "Nume",
"MoreInfo": "Mai multă informație",
"MoreInfo": "Mai multe informații",
"Message": "Mesaj",
"Logging": "Logare",
"LogFiles": "Fișiere de loguri",
@@ -103,7 +103,7 @@
"ShowAdvanced": "Arată setări avansate",
"Settings": "Setări",
"SetTags": "Setează Etichete",
"SelectAll": "SelecteazăTot",
"SelectAll": "Selectează tot",
"Seeders": "Partajatori",
"Security": "Securitate",
"Search": "Caută",
@@ -118,7 +118,7 @@
"Refresh": "Reîmprospătează",
"Queue": "Coadă",
"New": "Nou",
"OpenThisModal": "Deschideți acest mod",
"OpenThisModal": "Deschideți acest modal",
"SettingsEnableColorImpairedMode": "Activați modul afectat de culoare",
"SettingsEnableColorImpairedModeHelpText": "Stil modificat pentru a permite utilizatorilor cu deficiențe de culoare să distingă mai bine informațiile codificate prin culoare",
"SettingsLongDateFormat": "Format de dată lungă",
@@ -142,10 +142,10 @@
"AuthenticationMethodHelpText": "Solicitați numele de utilizator și parola pentru a accesa Prowlarr",
"AutomaticSearch": "Căutare automată",
"BackupFolderHelpText": "Căile relative vor fi în directorul AppData al lui Prowlarr",
"BackupIntervalHelpText": "Interval între crearea copiile de siguranță automate",
"Backups": "Copii de siguranță",
"BackupIntervalHelpText": "Interval între crearea copiile de rezervă automate",
"Backups": "Copii de rezervă",
"BeforeUpdate": "Înainte de actualizare",
"BindAddressHelpText": "Adresă IPv4 validă sau '*' pentru toate interfațele",
"BindAddressHelpText": "Adresă IP validă, localhost sau '*' pentru toate interfețele",
"Branch": "Ramură",
"BranchUpdate": "Ramură utilizată pentru a actualiza Prowlarr",
"BranchUpdateMechanism": "Ramură utilizată de mecanismul extern de actualizare",
@@ -153,7 +153,7 @@
"CancelPendingTask": "Sigur doriți să anulați această sarcină în așteptare?",
"CertificateValidation": "Validarea certificatului",
"CertificateValidationHelpText": "Modificați cât de strictă este validarea certificării HTTPS",
"ClientPriority": "Prioritatea clientului",
"ClientPriority": "Prioritate client",
"CloseCurrentModal": "Închideți modul curent",
"EnableAutomaticSearch": "Activați căutarea automată",
"EnableAutomaticSearchHelpText": "Va fi utilizat atunci când căutările automate sunt efectuate prin interfața de utilizare sau de către Prowlarr",
@@ -210,7 +210,7 @@
"Logs": "Jurnale",
"Mechanism": "Mecanism",
"MIA": "MIA",
"ProxyBypassFilterHelpText": "Folosiți „,” ca separator și *. ca un wildcard pentru subdomenii",
"ProxyBypassFilterHelpText": "Folosiți ',' ca separator și '*.' ca un wildcard pentru subdomenii",
"ProxyType": "Tip proxy",
"ProxyPasswordHelpText": "Trebuie să introduceți un nume de utilizator și o parolă numai dacă este necesară. Lasă-le necompletate altfel.",
"ProxyUsernameHelpText": "Trebuie să introduceți un nume de utilizator și o parolă numai dacă este necesară. Lasă-le necompletate altfel.",
@@ -226,7 +226,7 @@
"UpdateMechanismHelpText": "Utilizați actualizatorul încorporat al lui Prowlarr sau un script",
"AnalyticsEnabledHelpText": "Trimiteți informații anonime privind utilizarea și erorile către serverele Prowlarr. Aceasta include informații despre browserul folosit, ce pagini WebUI Prowlarr utilizați, raportarea erorilor, precum și sistemul de operare și versiunea de execuție. Vom folosi aceste informații pentru a acorda prioritate caracteristicilor și remedierilor de erori.",
"ApiKey": "Cheie API",
"BackupRetentionHelpText": "Copiile de siguranță automate mai vechi decât perioada de păstrare vor fi curățate automat",
"BackupRetentionHelpText": "Copiile de rezervă automate mai vechi decât perioada de păstrare vor fi curățate automat",
"BindAddress": "Adresa de legare",
"ChangeHasNotBeenSavedYet": "Modificarea nu a fost încă salvată",
"CloneProfile": "Clonați profil",
@@ -244,7 +244,7 @@
"IgnoredAddresses": "Adrese ignorate",
"IndexerLongTermStatusCheckAllClientMessage": "Toți indexatorii sunt indisponibili datorită erorilor de mai mult de 6 ore",
"IndexerLongTermStatusCheckSingleClientMessage": "Indexatori indisponibili datorită erorilor de mai mult de 6 ore: {0}",
"IndexerPriority": "Prioritatea indexatorului",
"IndexerPriority": "Prioritate indexator",
"Mode": "Mod",
"MovieIndexScrollTop": "Index film: Derulați sus",
"NoBackupsAreAvailable": "Nu sunt disponibile copii de rezervă",
@@ -271,11 +271,11 @@
"UnableToAddANewIndexerProxyPleaseTryAgain": "Nu se poate adăuga un nou proxy indexator, vă rugăm să încercați din nou.",
"UpdateScriptPathHelpText": "Calea către un script personalizat care preia un pachet de actualizare extras și se ocupă de restul procesului de actualizare",
"Uptime": "Timp de funcționare",
"UseProxy": "Utilizarea proxy",
"Username": "Nume de utilizator",
"UseProxy": "Utilizare proxy",
"Username": "Nume utilizator",
"Version": "Versiune",
"Yesterday": "Ieri",
"AcceptConfirmationModal": "Acceptați Modul de confirmare",
"AcceptConfirmationModal": "Acceptare modal de confirmare",
"DeleteIndexerProxyMessageText": "Sigur doriți să ștergeți proxyul „{0}”?",
"Disabled": "Dezactivat",
"Discord": "Discord",
@@ -297,7 +297,7 @@
"GeneralSettings": "Setări generale",
"Grabs": "Descărcări",
"Port": "Port",
"PortNumber": "Numarul portului",
"PortNumber": "Număr port",
"Reset": "Resetați",
"SendAnonymousUsageData": "Trimiteți date de utilizare anonime",
"SSLCertPassword": "Parola SSL Cert",
@@ -326,7 +326,7 @@
"GrabReleases": "Descarcă fișier(e)",
"MappedDrivesRunningAsService": "Unitățile de rețea mapate nu sunt disponibile atunci când rulează ca serviciu Windows. Vă rugăm să consultați FAQ pentru mai multe informații",
"No": "Nu",
"Yes": "da",
"Yes": "Da",
"IndexerSettingsSummary": "Configurați diverse setări globale indexatoare inclusiv proxiuri.",
"EnableRssHelpText": "Activați flux RSS pentru indexator",
"IndexerHealthCheckNoIndexers": "Niciun indexator nu este activat, Prowlarr nu va returna rezultate la căutare.",
@@ -396,5 +396,30 @@
"Label": "Etichetă",
"More": "Mai mult",
"ApplyTagsHelpTextReplace": "Înlocuire: înlocuiți etichetele cu etichetele introduse (nu introduceți etichete pentru a șterge toate etichetele)",
"Year": "An"
"Year": "An",
"ApplicationURL": "URL aplicație",
"Database": "Bază de date",
"ApplyChanges": "Aplicați modificări",
"InstanceName": "Nume instanță",
"QueryResults": "Rezultatele interogării",
"CountApplicationsSelected": "{0} aplicații selectate",
"Implementation": "Implementarea",
"NewznabUrl": "URL Newznab",
"TorznabUrl": "URL Torznab",
"Public": "Public",
"Parameters": "Parametri",
"GoToApplication": "Mergeți la aplicație",
"QueryType": "Tipul interogării",
"AuthQueries": "Interogări de autentificare",
"AreYouSureYouWantToDeleteIndexer": "Sigur doriți să ștergeți '{0}' din Prowlarr?",
"RssQueries": "Interogări RSS",
"SearchQueries": "Interogări de căutare",
"days": "zile",
"minutes": "minute",
"Private": "Privat",
"VipExpiration": "Expirare VIP",
"SemiPrivate": "Semi-Privat",
"Query": "Interogare",
"QueryOptions": "Opțiuni interogare",
"Publisher": "Editor"
}

View File

@@ -24,7 +24,7 @@
"ApiKey": "API 密钥",
"ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少{0}个字符长。您可以通过设置或配置文件执行此操作",
"AppDataDirectory": "AppData目录",
"AppDataLocationHealthCheckMessage": "更新将无法阻止在更新时删除 AppData",
"AppDataLocationHealthCheckMessage": "正在更新期间的 AppData 不会被更新删除",
"AppProfileDeleteConfirm": "您确认您想删除吗?",
"AppProfileInUse": "正在使用的应用程序配置文件",
"AppProfileSelectHelpText": "应用程序配置用于控制应用程序同步设置 RSS、自动搜索和交互式搜索设置",
@@ -497,7 +497,7 @@
"Album": "专辑",
"Track": "追踪",
"ApplyChanges": "应用更改",
"ApplyTagsHelpTextAdd": "添加:将标签添加到现有标签列表",
"ApplyTagsHelpTextAdd": "添加: 添加标签至已有的标签列表",
"CountDownloadClientsSelected": "已选择 {0} 个下载客户端",
"CountIndexersSelected": "已选择 {0} 个索引器",
"DeleteSelectedDownloadClientsMessageText": "您确定要删除 {0} 个选定的下载客户端吗?",
@@ -509,12 +509,13 @@
"NoIndexersFound": "未找到索引器",
"SelectIndexers": "搜刮器搜索",
"ApplyTagsHelpTextHowToApplyApplications": "如何给选中的电影添加标签",
"ApplyTagsHelpTextHowToApplyIndexers": "如何给选中的电影添加标签",
"ApplyTagsHelpTextReplace": "替换用输入的标签替换标签(不输入标签将清除所有标签",
"ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选择的索引器",
"ApplyTagsHelpTextReplace": "替换: 用输入的标签替换当前标签 (不输入将会清除所有标签)",
"DeleteSelectedApplicationsMessageText": "您确定要删除 {0} 个选定的导入列表吗?",
"DeleteSelectedDownloadClients": "删除下载客户端",
"DownloadClientPriorityHelpText": "优先考虑多个下载客户端,循环查询用于具有相同优先级的客户端。",
"EditSelectedIndexers": "编辑选定的索引器",
"OnHealthRestored": "健康度恢复",
"OnHealthRestoredHelpText": "健康度恢复"
"OnHealthRestoredHelpText": "健康度恢复",
"ApplyTagsHelpTextRemove": "移除: 移除已输入的标签"
}