mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
18 Commits
v1.7.3.376
...
v1.8.0.380
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1927e1e0f | ||
|
|
630a4ce800 | ||
|
|
8b1dd78300 | ||
|
|
cab50b35aa | ||
|
|
eee1be784b | ||
|
|
269dc5688b | ||
|
|
9bed795c89 | ||
|
|
3b5f151252 | ||
|
|
b3a541c9ff | ||
|
|
bc90fa2d3f | ||
|
|
4b0a896434 | ||
|
|
6be0e08635 | ||
|
|
f618901048 | ||
|
|
809ed940e6 | ||
|
|
7b14c2ee66 | ||
|
|
4528d03931 | ||
|
|
e0b30d34b1 | ||
|
|
8edf483e69 |
@@ -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)'
|
||||
|
||||
@@ -26,7 +26,8 @@ module.exports = {
|
||||
globals: {
|
||||
expect: false,
|
||||
chai: false,
|
||||
sinon: false
|
||||
sinon: false,
|
||||
JSX: true
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
{/*
|
||||
|
||||
@@ -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;
|
||||
|
||||
6
frontend/src/App/State/CommandAppState.ts
Normal file
6
frontend/src/App/State/CommandAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Command from 'Commands/Command';
|
||||
|
||||
export type CommandAppState = AppSectionState<Command>;
|
||||
|
||||
export default CommandAppState;
|
||||
@@ -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;
|
||||
|
||||
11
frontend/src/App/State/IndexerStatsAppState.ts
Normal file
11
frontend/src/App/State/IndexerStatsAppState.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
37
frontend/src/Commands/Command.ts
Normal file
37
frontend/src/Commands/Command.ts
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -68,6 +68,7 @@ class PathInputConnector extends Component {
|
||||
}
|
||||
|
||||
PathInputConnector.propTypes = {
|
||||
...PathInput.props,
|
||||
includeFiles: PropTypes.bool.isRequired,
|
||||
dispatchFetchPaths: PropTypes.func.isRequired,
|
||||
dispatchClearPaths: PropTypes.func.isRequired
|
||||
|
||||
7
frontend/src/Components/Table/Cells/TableRowCellButton.css.d.ts
vendored
Normal file
7
frontend/src/Components/Table/Cells/TableRowCellButton.css.d.ts
vendored
Normal 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;
|
||||
@@ -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;
|
||||
19
frontend/src/Components/Table/Cells/TableRowCellButton.tsx
Normal file
19
frontend/src/Components/Table/Cells/TableRowCellButton.tsx
Normal 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;
|
||||
@@ -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')}
|
||||
|
||||
@@ -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;
|
||||
25
frontend/src/Indexer/Delete/DeleteIndexerModal.tsx
Normal file
25
frontend/src/Indexer/Delete/DeleteIndexerModal.tsx
Normal 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;
|
||||
@@ -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;
|
||||
51
frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx
Normal file
51
frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx
Normal 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;
|
||||
@@ -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);
|
||||
@@ -12,7 +12,7 @@ interface IndexerStatusCellProps {
|
||||
className: string;
|
||||
enabled: boolean;
|
||||
redirect: boolean;
|
||||
status: IndexerStatus;
|
||||
status?: IndexerStatus;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
component?: React.ElementType;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
275
frontend/src/Indexer/Stats/IndexerStats.tsx
Normal file
275
frontend/src/Indexer/Stats/IndexerStats.tsx
Normal 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;
|
||||
27
frontend/src/Indexer/Stats/IndexerStatsFilterMenu.tsx
Normal file
27
frontend/src/Indexer/Stats/IndexerStatsFilterMenu.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
interface CssExports {
|
||||
'application': string;
|
||||
'enabled': string;
|
||||
'externalLink': string;
|
||||
'name': string;
|
||||
'nameContainer': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -104,6 +104,8 @@ export default {
|
||||
|
||||
[SELECT_INDEXER_PROXY_SCHEMA]: (state, { payload }) => {
|
||||
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||
selectedSchema.name = selectedSchema.implementationName;
|
||||
|
||||
return selectedSchema;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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) => {
|
||||
@@ -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;
|
||||
15
frontend/src/Store/Selectors/createIndexerStatusSelector.ts
Normal file
15
frontend/src/Store/Selectors/createIndexerStatusSelector.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
31
frontend/src/typings/IndexerStats.ts
Normal file
31
frontend/src/typings/IndexerStats.ts
Normal 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[];
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ namespace NzbDrone.Core.Applications
|
||||
|
||||
yield return new ApplicationDefinition
|
||||
{
|
||||
Name = GetType().Name,
|
||||
SyncLevel = ApplicationSyncLevel.FullSync,
|
||||
Implementation = GetType().Name,
|
||||
Settings = config
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "移除: 移除已输入的标签"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user