mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
39 Commits
v1.20.0.45
...
v1.21.2.46
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f67d2813a | ||
|
|
9a7a5fdc38 | ||
|
|
f1fdec6822 | ||
|
|
5464b23329 | ||
|
|
4c99971882 | ||
|
|
cc7769b601 | ||
|
|
cb2ed7daf9 | ||
|
|
78508094c8 | ||
|
|
b0f755a30c | ||
|
|
9d1384792a | ||
|
|
ea17116998 | ||
|
|
2c23681fc5 | ||
|
|
17aa2832ea | ||
|
|
5f3a329ef2 | ||
|
|
96f49da79e | ||
|
|
c7dfde0ce9 | ||
|
|
8cf32020f7 | ||
|
|
a5ed5a0e60 | ||
|
|
3279936fc9 | ||
|
|
8abccc709e | ||
|
|
76f30e7682 | ||
|
|
ab289b3e42 | ||
|
|
ef7e04065c | ||
|
|
d1084039b3 | ||
|
|
7bada440d2 | ||
|
|
803c4752db | ||
|
|
c0777474c0 | ||
|
|
66dcea5604 | ||
|
|
a2a12d2450 | ||
|
|
39593bd5a8 | ||
|
|
45d8a8a4e6 | ||
|
|
a4546c77ce | ||
|
|
d69bf6360a | ||
|
|
da9ce5b5c3 | ||
|
|
e092098101 | ||
|
|
1a89a79b74 | ||
|
|
cb6bf49922 | ||
|
|
4bcaba0be0 | ||
|
|
220ef723c7 |
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.20.0'
|
||||
majorVersion: '1.21.2'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.421'
|
||||
dotnetVersion: '6.0.424'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
@@ -1169,7 +1169,7 @@ stages:
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@1
|
||||
- task: SonarCloudPrepare@2
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
@@ -1187,21 +1187,16 @@ stages:
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@1
|
||||
- task: SonarCloudAnalyze@2
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@4
|
||||
- task: reportgenerator@5
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: Publish Coverage Report
|
||||
inputs:
|
||||
codeCoverageTool: 'cobertura'
|
||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
||||
reportDirectory: './CoverageResults/combined/'
|
||||
publishCodeCoverageResults: true
|
||||
|
||||
- stage: Report_Out
|
||||
dependsOn:
|
||||
|
||||
@@ -46,6 +46,10 @@ class StackedBarChart extends Component {
|
||||
size: 14,
|
||||
family: defaultFontFamily
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
position: 'average'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
|
||||
@@ -212,7 +213,7 @@ class FilterBuilderRow extends Component {
|
||||
key: name,
|
||||
value: typeof label === 'function' ? label() : label
|
||||
};
|
||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
||||
}).sort(sortByProp('value'));
|
||||
|
||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { filterBuilderTypes } from 'Helpers/Props';
|
||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createTagListSelector() {
|
||||
@@ -38,7 +38,7 @@ function createTagListSelector() {
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).sort(sortByName);
|
||||
}, []).sort(sortByProp('name'));
|
||||
}
|
||||
|
||||
return _.uniqBy(items, 'id');
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFilter from './CustomFilter';
|
||||
import styles from './CustomFiltersModalContent.css';
|
||||
@@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) {
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.sort((a, b) => sortByProp(a, b, 'label'))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
|
||||
@@ -4,13 +4,13 @@ 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 sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
||||
createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(appProfiles, includeNoChange, includeMixed) => {
|
||||
|
||||
@@ -3,7 +3,8 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -21,7 +22,7 @@ function createMapStateToProps() {
|
||||
|
||||
const values = items
|
||||
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
|
||||
.sort(sortByName)
|
||||
.sort(sortByProp('name'))
|
||||
.map((downloadClient) => ({
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name,
|
||||
@@ -31,7 +32,7 @@ function createMapStateToProps() {
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
value: `(${translate('Any')})`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ 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 sortByProp from 'Utilities/Array/sortByProp';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
createSortedSectionSelector('indexers', sortByName),
|
||||
createSortedSectionSelector('indexers', sortByProp('name')),
|
||||
(value, indexers) => {
|
||||
const values = [];
|
||||
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterMenuItem from './FilterMenuItem';
|
||||
import MenuContent from './MenuContent';
|
||||
@@ -47,7 +48,7 @@ class FilterMenuContent extends Component {
|
||||
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.sort(sortByProp('label'))
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Label from './Label';
|
||||
import styles from './TagList.css';
|
||||
|
||||
function TagList({ tags, tagList }) {
|
||||
const sortedTags = tags
|
||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||
.filter((t) => t !== undefined)
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
.filter((tag) => !!tag)
|
||||
.sort(sortByProp('label'));
|
||||
|
||||
return (
|
||||
<div className={styles.tags}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Link from 'Components/Link/Link';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryDetails.css';
|
||||
|
||||
@@ -10,7 +11,10 @@ function HistoryDetails(props) {
|
||||
const {
|
||||
indexer,
|
||||
eventType,
|
||||
data
|
||||
date,
|
||||
data,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (eventType === 'indexerQuery' || eventType === 'indexerRss') {
|
||||
@@ -22,7 +26,9 @@ function HistoryDetails(props) {
|
||||
offset,
|
||||
source,
|
||||
host,
|
||||
url
|
||||
url,
|
||||
elapsedTime,
|
||||
cached
|
||||
} = data;
|
||||
|
||||
return (
|
||||
@@ -104,6 +110,24 @@ function HistoryDetails(props) {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
elapsedTime ?
|
||||
<DescriptionListItem
|
||||
title={translate('ElapsedTime')}
|
||||
data={`${elapsedTime}ms${cached === '1' ? ' (cached)' : ''}`}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
date ?
|
||||
<DescriptionListItem
|
||||
title={translate('Date')}
|
||||
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
@@ -111,10 +135,19 @@ function HistoryDetails(props) {
|
||||
if (eventType === 'releaseGrabbed') {
|
||||
const {
|
||||
source,
|
||||
host,
|
||||
grabTitle,
|
||||
url
|
||||
url,
|
||||
publishedDate,
|
||||
infoUrl,
|
||||
downloadClient,
|
||||
downloadClientName,
|
||||
elapsedTime,
|
||||
grabMethod
|
||||
} = data;
|
||||
|
||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
{
|
||||
@@ -135,6 +168,15 @@ function HistoryDetails(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Host')}
|
||||
data={host}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
@@ -144,6 +186,33 @@ function HistoryDetails(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
infoUrl ?
|
||||
<DescriptionListItem
|
||||
title={translate('InfoUrl')}
|
||||
data={<Link to={infoUrl}>{infoUrl}</Link>}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
publishedDate ?
|
||||
<DescriptionListItem
|
||||
title={translate('PublishedDate')}
|
||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
downloadClientNameInfo ?
|
||||
<DescriptionListItem
|
||||
title={translate('DownloadClient')}
|
||||
data={downloadClientNameInfo}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
@@ -152,11 +221,40 @@ function HistoryDetails(props) {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
elapsedTime ?
|
||||
<DescriptionListItem
|
||||
title={translate('ElapsedTime')}
|
||||
data={`${elapsedTime}ms`}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
grabMethod ?
|
||||
<DescriptionListItem
|
||||
title={translate('Redirected')}
|
||||
data={grabMethod.toLowerCase() === 'redirect' ? translate('Yes') : translate('No')}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
date ?
|
||||
<DescriptionListItem
|
||||
title={translate('Date')}
|
||||
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'indexerAuth') {
|
||||
const { elapsedTime } = data;
|
||||
|
||||
return (
|
||||
<DescriptionList
|
||||
descriptionClassName={styles.description}
|
||||
@@ -170,6 +268,24 @@ function HistoryDetails(props) {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
elapsedTime ?
|
||||
<DescriptionListItem
|
||||
title={translate('ElapsedTime')}
|
||||
data={`${elapsedTime}ms`}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
date ?
|
||||
<DescriptionListItem
|
||||
title={translate('Date')}
|
||||
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
@@ -181,6 +297,15 @@ function HistoryDetails(props) {
|
||||
title={translate('Name')}
|
||||
data={data.query}
|
||||
/>
|
||||
|
||||
{
|
||||
date ?
|
||||
<DescriptionListItem
|
||||
title={translate('Date')}
|
||||
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
@@ -188,6 +313,7 @@ function HistoryDetails(props) {
|
||||
HistoryDetails.propTypes = {
|
||||
indexer: PropTypes.object.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
|
||||
@@ -29,6 +29,7 @@ function HistoryDetailsModal(props) {
|
||||
isOpen,
|
||||
eventType,
|
||||
indexer,
|
||||
date,
|
||||
data,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
@@ -49,6 +50,7 @@ function HistoryDetailsModal(props) {
|
||||
<HistoryDetails
|
||||
eventType={eventType}
|
||||
indexer={indexer}
|
||||
date={date}
|
||||
data={data}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
@@ -71,6 +73,7 @@ HistoryDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.object.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -20,21 +20,22 @@ function getIconName(eventType) {
|
||||
}
|
||||
}
|
||||
|
||||
function getIconKind(successful) {
|
||||
switch (successful) {
|
||||
case false:
|
||||
return kinds.DANGER;
|
||||
default:
|
||||
return kinds.DEFAULT;
|
||||
function getIconKind(successful, redirect) {
|
||||
if (redirect) {
|
||||
return kinds.INFO;
|
||||
} else if (!successful) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getTooltip(eventType, data, indexer) {
|
||||
function getTooltip(eventType, data, indexer, redirect) {
|
||||
switch (eventType) {
|
||||
case 'indexerQuery':
|
||||
return `Query "${data.query}" sent to ${indexer.name}`;
|
||||
case 'releaseGrabbed':
|
||||
return `Release grabbed from ${indexer.name}`;
|
||||
return redirect ? `Release grabbed via redirect from ${indexer.name}` : `Release grabbed from ${indexer.name}`;
|
||||
case 'indexerAuth':
|
||||
return `Auth attempted for ${indexer.name}`;
|
||||
case 'indexerRss':
|
||||
@@ -45,9 +46,12 @@ function getTooltip(eventType, data, indexer) {
|
||||
}
|
||||
|
||||
function HistoryEventTypeCell({ eventType, successful, data, indexer }) {
|
||||
const { grabMethod } = data;
|
||||
const redirect = grabMethod && grabMethod.toLowerCase() === 'redirect';
|
||||
|
||||
const iconName = getIconName(eventType);
|
||||
const iconKind = getIconKind(successful);
|
||||
const tooltip = getTooltip(eventType, data, indexer);
|
||||
const iconKind = getIconKind(successful, redirect);
|
||||
const tooltip = getTooltip(eventType, data, indexer, redirect);
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -370,8 +370,9 @@ class HistoryRow extends Component {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={date}
|
||||
className={styles.date}
|
||||
date={date}
|
||||
includeSeconds={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -408,6 +409,7 @@ class HistoryRow extends Component {
|
||||
<HistoryDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
eventType={eventType}
|
||||
date={date}
|
||||
data={data}
|
||||
indexer={indexer}
|
||||
isMarkingAsFailed={isMarkingAsFailed}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
|
||||
import styles from './AddIndexerModal.css';
|
||||
|
||||
function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
onModalClose={onModalClose}
|
||||
className={styles.modal}
|
||||
>
|
||||
<AddIndexerModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
onSelectIndexer={onSelectIndexer}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddIndexerModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSelectIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerModal;
|
||||
44
frontend/src/Indexer/Add/AddIndexerModal.tsx
Normal file
44
frontend/src/Indexer/Add/AddIndexerModal.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { clearIndexerSchema } from 'Store/Actions/indexerActions';
|
||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||
import styles from './AddIndexerModal.css';
|
||||
|
||||
interface AddIndexerModalProps {
|
||||
isOpen: boolean;
|
||||
onSelectIndexer(): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function AddIndexerModal({
|
||||
isOpen,
|
||||
onSelectIndexer,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: AddIndexerModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onModalClosePress = useCallback(() => {
|
||||
dispatch(clearIndexerSchema());
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
onModalClose={onModalClosePress}
|
||||
className={styles.modal}
|
||||
>
|
||||
<AddIndexerModalContent
|
||||
{...otherProps}
|
||||
onSelectIndexer={onSelectIndexer}
|
||||
onModalClose={onModalClosePress}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddIndexerModal;
|
||||
@@ -19,12 +19,18 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
.notice {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ interface CssExports {
|
||||
'indexers': string;
|
||||
'modalBody': string;
|
||||
'modalFooter': string;
|
||||
'notice': string;
|
||||
'scroller': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
|
||||
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Protocol'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sortName',
|
||||
label: () => translate('Name'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: () => translate('Description'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'privacy',
|
||||
label: () => translate('Privacy'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
label: () => translate('Categories'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
const protocols = [
|
||||
{
|
||||
key: 'torrent',
|
||||
value: 'torrent'
|
||||
},
|
||||
{
|
||||
key: 'usenet',
|
||||
value: 'nzb'
|
||||
}
|
||||
];
|
||||
|
||||
const privacyLevels = [
|
||||
{
|
||||
key: 'private',
|
||||
get value() {
|
||||
return translate('Private');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'semiPrivate',
|
||||
get value() {
|
||||
return translate('SemiPrivate');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'public',
|
||||
get value() {
|
||||
return translate('Public');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class AddIndexerModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
filter: '',
|
||||
filterProtocols: [],
|
||||
filterLanguages: [],
|
||||
filterPrivacyLevels: [],
|
||||
filterCategories: []
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFilterChange = ({ value }) => {
|
||||
this.setState({ filter: value });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
indexers,
|
||||
onIndexerSelect,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
onSortPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const languages = Array.from(new Set(indexers.map(({ language }) => language)))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((language) => ({ key: language, value: language }));
|
||||
|
||||
const filteredIndexers = indexers.filter((indexer) => {
|
||||
const {
|
||||
filter,
|
||||
filterProtocols,
|
||||
filterLanguages,
|
||||
filterPrivacyLevels,
|
||||
filterCategories
|
||||
} = this.state;
|
||||
|
||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterLanguages.length && !filterLanguages.includes(indexer.language)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterCategories.length) {
|
||||
const { categories = [] } = indexer.capabilities || {};
|
||||
const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)];
|
||||
const flatCategories = categories
|
||||
.filter((item) => item.id < 100000)
|
||||
.flatMap(flat);
|
||||
|
||||
if (!filterCategories.every((item) => flatCategories.includes(item))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AddIndexer')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder={translate('FilterPlaceHolder')}
|
||||
name="filter"
|
||||
value={this.state.filter}
|
||||
autoFocus={true}
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Protocol')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerProtocols"
|
||||
value={this.state.filterProtocols}
|
||||
values={protocols}
|
||||
onChange={({ value }) => this.setState({ filterProtocols: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Language')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerLanguages"
|
||||
value={this.state.filterLanguages}
|
||||
values={languages}
|
||||
onChange={({ value }) => this.setState({ filterLanguages: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Privacy')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerPrivacyLevels"
|
||||
value={this.state.filterPrivacyLevels}
|
||||
values={privacyLevels}
|
||||
onChange={({ value }) => this.setState({ filterPrivacyLevels: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Categories')}</label>
|
||||
<NewznabCategorySelectInputConnector
|
||||
name="indexerCategories"
|
||||
value={this.state.filterCategories}
|
||||
onChange={({ value }) => this.setState({ filterCategories: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
kind={kinds.INFO}
|
||||
className={styles.alert}
|
||||
>
|
||||
<div>
|
||||
{translate('ProwlarrSupportsAnyIndexer')}
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
autoFocus={false}
|
||||
>
|
||||
{
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
{
|
||||
error ? <Alert kind={kinds.DANGER}>{errorMessage}</Alert> : null
|
||||
}
|
||||
{
|
||||
isPopulated && !!indexers.length ?
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
filteredIndexers.map((indexer) => (
|
||||
<SelectIndexerRow
|
||||
key={`${indexer.implementation}-${indexer.name}`}
|
||||
implementation={indexer.implementation}
|
||||
implementationName={indexer.implementationName}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
{
|
||||
isPopulated && !!indexers.length && !filteredIndexers.length ?
|
||||
<Alert
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('NoIndexersFound')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div className={styles.available}>
|
||||
{
|
||||
isPopulated ?
|
||||
translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onIndexerSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerModalContent;
|
||||
414
frontend/src/Indexer/Add/AddIndexerModalContent.tsx
Normal file
414
frontend/src/Indexer/Add/AddIndexerModalContent.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
import { some } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import IndexerAppState from 'App/State/IndexerAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
|
||||
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import Indexer, { IndexerCategory } from 'Indexer/Indexer';
|
||||
import {
|
||||
fetchIndexerSchema,
|
||||
selectIndexerSchema,
|
||||
setIndexerSchemaSort,
|
||||
} from 'Store/Actions/indexerActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SortCallback } from 'typings/callbacks';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Protocol'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'sortName',
|
||||
label: () => translate('Name'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: () => translate('Description'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'privacy',
|
||||
label: () => translate('Privacy'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
label: () => translate('Categories'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
const PROTOCOLS = [
|
||||
{
|
||||
key: 'torrent',
|
||||
value: 'torrent',
|
||||
},
|
||||
{
|
||||
key: 'usenet',
|
||||
value: 'nzb',
|
||||
},
|
||||
];
|
||||
|
||||
const PRIVACY_LEVELS = [
|
||||
{
|
||||
key: 'private',
|
||||
get value() {
|
||||
return translate('Private');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'semiPrivate',
|
||||
get value() {
|
||||
return translate('SemiPrivate');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'public',
|
||||
get value() {
|
||||
return translate('Public');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface IndexerSchema extends Indexer {
|
||||
isExistingIndexer: boolean;
|
||||
}
|
||||
|
||||
function createAddIndexersSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('indexers.schema'),
|
||||
createAllIndexersSelector(),
|
||||
(indexers: IndexerAppState, allIndexers) => {
|
||||
const { isFetching, isPopulated, error, items, sortDirection, sortKey } =
|
||||
indexers;
|
||||
|
||||
const indexerList: IndexerSchema[] = items.map((item) => {
|
||||
const { definitionName } = item;
|
||||
return {
|
||||
...item,
|
||||
isExistingIndexer: some(allIndexers, { definitionName }),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
indexers: indexerList,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface AddIndexerModalContentProps {
|
||||
onSelectIndexer(): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function AddIndexerModalContent(props: AddIndexerModalContentProps) {
|
||||
const { onSelectIndexer, onModalClose } = props;
|
||||
|
||||
const { isFetching, isPopulated, error, indexers, sortKey, sortDirection } =
|
||||
useSelector(createAddIndexersSelector());
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
const [filterProtocols, setFilterProtocols] = useState<string[]>([]);
|
||||
const [filterLanguages, setFilterLanguages] = useState<string[]>([]);
|
||||
const [filterPrivacyLevels, setFilterPrivacyLevels] = useState<string[]>([]);
|
||||
const [filterCategories, setFilterCategories] = useState<number[]>([]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
dispatch(fetchIndexerSchema());
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
setFilter(value);
|
||||
},
|
||||
[setFilter]
|
||||
);
|
||||
|
||||
const onIndexerSelect = useCallback(
|
||||
({
|
||||
implementation,
|
||||
implementationName,
|
||||
name,
|
||||
}: {
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
name: string;
|
||||
}) => {
|
||||
dispatch(
|
||||
selectIndexerSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
name,
|
||||
})
|
||||
);
|
||||
|
||||
onSelectIndexer();
|
||||
},
|
||||
[dispatch, onSelectIndexer]
|
||||
);
|
||||
|
||||
const onSortPress = useCallback<SortCallback>(
|
||||
(sortKey, sortDirection) => {
|
||||
dispatch(setIndexerSchemaSort({ sortKey, sortDirection }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const languages = useMemo(
|
||||
() =>
|
||||
Array.from(new Set(indexers.map(({ language }) => language)))
|
||||
.map((language) => ({ key: language, value: language }))
|
||||
.sort(sortByProp('value')),
|
||||
[indexers]
|
||||
);
|
||||
|
||||
const filteredIndexers = useMemo(() => {
|
||||
const flat = ({
|
||||
id,
|
||||
subCategories = [],
|
||||
}: {
|
||||
id: number;
|
||||
subCategories: IndexerCategory[];
|
||||
}): number[] => [id, ...subCategories.flatMap(flat)];
|
||||
|
||||
return indexers.filter((indexer) => {
|
||||
if (
|
||||
filter.length &&
|
||||
!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) &&
|
||||
!indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filterProtocols.length &&
|
||||
!filterProtocols.includes(indexer.protocol)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filterLanguages.length &&
|
||||
!filterLanguages.includes(indexer.language)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filterPrivacyLevels.length &&
|
||||
!filterPrivacyLevels.includes(indexer.privacy)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterCategories.length) {
|
||||
const { categories = [] } = indexer.capabilities || {};
|
||||
|
||||
const flatCategories = categories
|
||||
.filter((item) => item.id < 100000)
|
||||
.flatMap(flat);
|
||||
|
||||
if (
|
||||
!filterCategories.every((categoryId) =>
|
||||
flatCategories.includes(categoryId)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [
|
||||
indexers,
|
||||
filter,
|
||||
filterProtocols,
|
||||
filterLanguages,
|
||||
filterPrivacyLevels,
|
||||
filterCategories,
|
||||
]);
|
||||
|
||||
const errorMessage = getErrorMessage(
|
||||
error,
|
||||
translate('UnableToLoadIndexers')
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder={translate('FilterPlaceHolder')}
|
||||
name="filter"
|
||||
value={filter}
|
||||
autoFocus={true}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>
|
||||
{translate('Protocol')}
|
||||
</label>
|
||||
|
||||
<EnhancedSelectInput
|
||||
name="indexerProtocols"
|
||||
value={filterProtocols}
|
||||
values={PROTOCOLS}
|
||||
onChange={({ value }: { value: string[] }) =>
|
||||
setFilterProtocols(value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>
|
||||
{translate('Language')}
|
||||
</label>
|
||||
|
||||
<EnhancedSelectInput
|
||||
name="indexerLanguages"
|
||||
value={filterLanguages}
|
||||
values={languages}
|
||||
onChange={({ value }: { value: string[] }) =>
|
||||
setFilterLanguages(value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Privacy')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerPrivacyLevels"
|
||||
value={filterPrivacyLevels}
|
||||
values={PRIVACY_LEVELS}
|
||||
onChange={({ value }: { value: string[] }) =>
|
||||
setFilterPrivacyLevels(value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>
|
||||
{translate('Categories')}
|
||||
</label>
|
||||
|
||||
<NewznabCategorySelectInputConnector
|
||||
name="indexerCategories"
|
||||
value={filterCategories}
|
||||
onChange={({ value }: { value: number[] }) =>
|
||||
setFilterCategories(value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert kind={kinds.INFO} className={styles.notice}>
|
||||
<div>{translate('ProwlarrSupportsAnyIndexer')}</div>
|
||||
</Alert>
|
||||
|
||||
<Scroller className={styles.scroller} autoFocus={false}>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{error ? (
|
||||
<Alert kind={kinds.DANGER} className={styles.alert}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!indexers.length ? (
|
||||
<Table
|
||||
columns={COLUMNS}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{filteredIndexers.map((indexer) => (
|
||||
<SelectIndexerRow
|
||||
{...indexer}
|
||||
key={`${indexer.implementation}-${indexer.name}`}
|
||||
implementation={indexer.implementation}
|
||||
implementationName={indexer.implementationName}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!indexers.length && !filteredIndexers.length ? (
|
||||
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||
{translate('NoIndexersFound')}
|
||||
</Alert>
|
||||
) : null}
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div className={styles.available}>
|
||||
{isPopulated
|
||||
? translate('CountIndexersAvailable', {
|
||||
count: filteredIndexers.length,
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddIndexerModalContent;
|
||||
@@ -1,94 +0,0 @@
|
||||
import { some } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('indexers.schema'),
|
||||
createAllIndexersSelector(),
|
||||
(indexers, allIndexers) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
sortDirection,
|
||||
sortKey
|
||||
} = indexers;
|
||||
|
||||
const indexerList = items.map((item) => {
|
||||
const { definitionName } = item;
|
||||
return {
|
||||
...item,
|
||||
isExistingIndexer: some(allIndexers, { definitionName })
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
indexers: indexerList,
|
||||
sortKey,
|
||||
sortDirection
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexerSchema,
|
||||
selectIndexerSchema,
|
||||
setIndexerSchemaSort
|
||||
};
|
||||
|
||||
class AddIndexerModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexerSchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerSelect = ({ implementation, implementationName, name }) => {
|
||||
this.props.selectIndexerSchema({ implementation, implementationName, name });
|
||||
this.props.onSelectIndexer();
|
||||
};
|
||||
|
||||
onSortPress = (sortKey, sortDirection) => {
|
||||
this.props.setIndexerSchemaSort({ sortKey, sortDirection });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddIndexerModalContent
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onIndexerSelect={this.onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerModalContentConnector.propTypes = {
|
||||
fetchIndexerSchema: PropTypes.func.isRequired,
|
||||
selectIndexerSchema: PropTypes.func.isRequired,
|
||||
setIndexerSchemaSort: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSelectIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);
|
||||
@@ -38,6 +38,7 @@ export interface IndexerField extends ModelBase {
|
||||
|
||||
interface Indexer extends ModelBase {
|
||||
name: string;
|
||||
definitionName: string;
|
||||
description: string;
|
||||
encoding: string;
|
||||
language: string;
|
||||
|
||||
@@ -74,7 +74,7 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) {
|
||||
</div>
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCell date={date} />
|
||||
<RelativeDateCell date={date} includeSeconds={true} />
|
||||
|
||||
<TableRowCell className={styles.source}>
|
||||
{data.source ? data.source : null}
|
||||
@@ -91,6 +91,7 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) {
|
||||
<HistoryDetailsModal
|
||||
isOpen={isDetailsModalOpen}
|
||||
eventType={eventType}
|
||||
date={date}
|
||||
data={data}
|
||||
indexer={indexer}
|
||||
shortDateFormat={shortDateFormat}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -14,11 +15,9 @@ function NoIndexer(props: NoIndexerProps) {
|
||||
|
||||
if (totalItems > 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
{translate('AllIndexersHiddenDueToFilter')}
|
||||
</div>
|
||||
</div>
|
||||
<Alert kind={kinds.WARNING} className={styles.message}>
|
||||
{translate('AllIndexersHiddenDueToFilter')}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,136 +32,125 @@ import IndexerStatsFilterModal from './IndexerStatsFilterModal';
|
||||
import styles from './IndexerStats.css';
|
||||
|
||||
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
|
||||
const data = indexerStats.map((indexer) => {
|
||||
return {
|
||||
label: indexer.indexerName,
|
||||
value: indexer.averageResponseTime,
|
||||
};
|
||||
});
|
||||
const statistics = [...indexerStats].sort((a, b) =>
|
||||
a.averageResponseTime === b.averageResponseTime
|
||||
? b.averageGrabResponseTime - a.averageGrabResponseTime
|
||||
: b.averageResponseTime - a.averageResponseTime
|
||||
);
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
|
||||
return data;
|
||||
return {
|
||||
labels: statistics.map((indexer) => indexer.indexerName),
|
||||
datasets: [
|
||||
{
|
||||
label: translate('AverageQueries'),
|
||||
data: statistics.map((indexer) => indexer.averageResponseTime),
|
||||
},
|
||||
{
|
||||
label: translate('AverageGrabs'),
|
||||
data: statistics.map((indexer) => indexer.averageGrabResponseTime),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
});
|
||||
const data = indexerStats.map((indexer) => ({
|
||||
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;
|
||||
});
|
||||
data.sort((a, b) => b.value - a.value);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) {
|
||||
const data = {
|
||||
labels: indexerStats.map((indexer) => indexer.indexerName),
|
||||
const statistics = [...indexerStats].sort(
|
||||
(a, b) =>
|
||||
b.numberOfQueries +
|
||||
b.numberOfRssQueries +
|
||||
b.numberOfAuthQueries -
|
||||
(a.numberOfQueries + a.numberOfRssQueries + a.numberOfAuthQueries)
|
||||
);
|
||||
|
||||
return {
|
||||
labels: statistics.map((indexer) => indexer.indexerName),
|
||||
datasets: [
|
||||
{
|
||||
label: translate('SearchQueries'),
|
||||
data: indexerStats.map((indexer) => indexer.numberOfQueries),
|
||||
data: statistics.map((indexer) => indexer.numberOfQueries),
|
||||
},
|
||||
{
|
||||
label: translate('RssQueries'),
|
||||
data: indexerStats.map((indexer) => indexer.numberOfRssQueries),
|
||||
data: statistics.map((indexer) => indexer.numberOfRssQueries),
|
||||
},
|
||||
{
|
||||
label: translate('AuthQueries'),
|
||||
data: indexerStats.map((indexer) => indexer.numberOfAuthQueries),
|
||||
data: statistics.map((indexer) => indexer.numberOfAuthQueries),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) {
|
||||
const data = indexerStats.map((indexer) => {
|
||||
return {
|
||||
label: indexer.indexerName,
|
||||
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
|
||||
};
|
||||
});
|
||||
const data = indexerStats.map((indexer) => ({
|
||||
label: indexer.indexerName,
|
||||
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
|
||||
}));
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
data.sort((a, b) => 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,
|
||||
};
|
||||
});
|
||||
const data = indexerStats.map((indexer) => ({
|
||||
label: indexer.userAgent ? indexer.userAgent : 'Other',
|
||||
value: indexer.numberOfGrabs,
|
||||
}));
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
data.sort((a, b) => 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,
|
||||
};
|
||||
});
|
||||
const data = indexerStats.map((indexer) => ({
|
||||
label: indexer.userAgent ? indexer.userAgent : 'Other',
|
||||
value: indexer.numberOfQueries,
|
||||
}));
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
data.sort((a, b) => 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,
|
||||
};
|
||||
});
|
||||
const data = indexerStats.map((indexer) => ({
|
||||
label: indexer.host ? indexer.host : 'Other',
|
||||
value: indexer.numberOfGrabs,
|
||||
}));
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
data.sort((a, b) => 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,
|
||||
};
|
||||
});
|
||||
const data = indexerStats.map((indexer) => ({
|
||||
label: indexer.host ? indexer.host : 'Other',
|
||||
value: indexer.numberOfQueries,
|
||||
}));
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
data.sort((a, b) => b.value - a.value);
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -294,7 +283,7 @@ function IndexerStats() {
|
||||
</div>
|
||||
<div className={styles.fullWidthChart}>
|
||||
<div className={styles.chartContainer}>
|
||||
<BarChart
|
||||
<StackedBarChart
|
||||
data={getAverageResponseTimeData(item.indexers)}
|
||||
title={translate('AverageResponseTimesMs')}
|
||||
stepSize={100}
|
||||
|
||||
@@ -231,7 +231,9 @@ function SearchIndexOverview(props: SearchIndexOverviewProps) {
|
||||
|
||||
{indexerFlags.length
|
||||
? indexerFlags
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.sort((a, b) =>
|
||||
a.localeCompare(b, undefined, { numeric: true })
|
||||
)
|
||||
.map((flag, index) => {
|
||||
return (
|
||||
<Label key={index} kind={kinds.INFO}>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './NoSearchResults.css';
|
||||
|
||||
@@ -11,18 +13,16 @@ function NoSearchResults(props: NoSearchResultsProps) {
|
||||
|
||||
if (totalItems > 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
{translate('AllIndexersHiddenDueToFilter')}
|
||||
</div>
|
||||
</div>
|
||||
<Alert kind={kinds.WARNING} className={styles.message}>
|
||||
{translate('AllSearchResultsHiddenByFilter')}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>{translate('NoSearchResultsFound')}</div>
|
||||
</div>
|
||||
<Alert kind={kinds.INFO} className={styles.message}>
|
||||
{translate('NoSearchResultsFound')}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ class SearchIndex extends Component {
|
||||
|
||||
const ViewComponent = getViewComponent(isSmallScreen);
|
||||
const isLoaded = !!(!error && isPopulated && items.length && this.scrollerRef.current);
|
||||
const hasNoIndexer = !totalItems;
|
||||
const hasNoSearchResults = !totalItems;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Search')}>
|
||||
@@ -306,7 +306,7 @@ class SearchIndex extends Component {
|
||||
<SearchIndexSortMenu
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
isDisabled={hasNoIndexer}
|
||||
isDisabled={hasNoSearchResults}
|
||||
onSortSelect={onSortSelect}
|
||||
/>
|
||||
|
||||
@@ -314,7 +314,7 @@ class SearchIndex extends Component {
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
isDisabled={hasNoIndexer}
|
||||
isDisabled={hasNoSearchResults}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
@@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
|
||||
import { deleteApplication, fetchApplications } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Applications from './Applications';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.applications', sortByName),
|
||||
createSortedSectionSelector('settings.applications', sortByProp('name')),
|
||||
createTagsSelector(),
|
||||
(applications, tagList) => {
|
||||
return {
|
||||
|
||||
@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import DownloadClients from './DownloadClients';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.downloadClients', sortByName),
|
||||
createSortedSectionSelector('settings.downloadClients', sortByProp('name')),
|
||||
(downloadClients) => downloadClients
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ function UpdateSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
isWindows,
|
||||
packageUpdateMechanism,
|
||||
onInputChange
|
||||
} = props;
|
||||
@@ -38,10 +37,10 @@ function UpdateSettings(props) {
|
||||
value: titleCase(packageUpdateMechanism)
|
||||
});
|
||||
} else {
|
||||
updateOptions.push({ key: 'builtIn', value: 'Built-In' });
|
||||
updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') });
|
||||
}
|
||||
|
||||
updateOptions.push({ key: 'script', value: 'Script' });
|
||||
updateOptions.push({ key: 'script', value: translate('Script') });
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Updates')}>
|
||||
@@ -62,61 +61,58 @@ function UpdateSettings(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
!isWindows &&
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Automatic')}</FormLabel>
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Automatic')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('Mechanism')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wiki.servarr.com/prowlarr/settings#updates"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('Mechanism')}</FormLabel>
|
||||
<FormLabel>{translate('ScriptPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wiki.servarr.com/prowlarr/settings#updates"
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText={translate('UpdateScriptPathHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ScriptPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText={translate('UpdateScriptPathHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import { createSelector } from 'reselect';
|
||||
import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import IndexerProxies from './IndexerProxies';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.indexerProxies', sortByName),
|
||||
createSortedSectionSelector('indexers', sortByName),
|
||||
createSortedSectionSelector('settings.indexerProxies', sortByProp('name')),
|
||||
createSortedSectionSelector('indexers', sortByProp('name')),
|
||||
createTagsSelector(),
|
||||
(indexerProxies, indexers, tagList) => {
|
||||
return {
|
||||
|
||||
@@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
|
||||
import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Notifications from './Notifications';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.notifications', sortByName),
|
||||
createSortedSectionSelector('settings.notifications', sortByProp('name')),
|
||||
createTagsSelector(),
|
||||
(notifications, tagList) => {
|
||||
return {
|
||||
|
||||
@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cloneAppProfile, deleteAppProfile, fetchAppProfiles } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import AppProfiles from './AppProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
||||
createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
|
||||
(appProfiles) => appProfiles
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@ import { createAction } from 'redux-actions';
|
||||
import { filterTypePredicates, sortDirections } from 'Helpers/Props';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createSaveProviderHandler, {
|
||||
createCancelSaveProviderHandler
|
||||
} from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||
import createTestProviderHandler, {
|
||||
createCancelTestProviderHandler
|
||||
} from 'Store/Actions/Creators/createTestProviderHandler';
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
@@ -16,6 +20,7 @@ import translate from 'Utilities/String/translate';
|
||||
import createBulkEditItemHandler from './Creators/createBulkEditItemHandler';
|
||||
import createBulkRemoveItemHandler from './Creators/createBulkRemoveItemHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createClearReducer from './Creators/Reducers/createClearReducer';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
|
||||
//
|
||||
@@ -96,10 +101,7 @@ export const filterPredicates = {
|
||||
|
||||
export const sortPredicates = {
|
||||
vipExpiration: function(item) {
|
||||
const vipExpiration =
|
||||
item.fields.find((field) => field.name === 'vipExpiration')?.value ?? '';
|
||||
|
||||
return vipExpiration;
|
||||
return item.fields.find((field) => field.name === 'vipExpiration')?.value ?? '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,6 +112,7 @@ export const FETCH_INDEXERS = 'indexers/fetchIndexers';
|
||||
export const FETCH_INDEXER_SCHEMA = 'indexers/fetchIndexerSchema';
|
||||
export const SELECT_INDEXER_SCHEMA = 'indexers/selectIndexerSchema';
|
||||
export const SET_INDEXER_SCHEMA_SORT = 'indexers/setIndexerSchemaSort';
|
||||
export const CLEAR_INDEXER_SCHEMA = 'indexers/clearIndexerSchema';
|
||||
export const CLONE_INDEXER = 'indexers/cloneIndexer';
|
||||
export const SET_INDEXER_VALUE = 'indexers/setIndexerValue';
|
||||
export const SET_INDEXER_FIELD_VALUE = 'indexers/setIndexerFieldValue';
|
||||
@@ -129,6 +132,7 @@ export const fetchIndexers = createThunk(FETCH_INDEXERS);
|
||||
export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA);
|
||||
export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA);
|
||||
export const setIndexerSchemaSort = createAction(SET_INDEXER_SCHEMA_SORT);
|
||||
export const clearIndexerSchema = createAction(CLEAR_INDEXER_SCHEMA);
|
||||
export const cloneIndexer = createAction(CLONE_INDEXER);
|
||||
|
||||
export const saveIndexer = createThunk(SAVE_INDEXER);
|
||||
@@ -214,6 +218,8 @@ export const reducers = createHandleActions({
|
||||
});
|
||||
},
|
||||
|
||||
[CLEAR_INDEXER_SCHEMA]: createClearReducer(schemaSection, defaultState),
|
||||
|
||||
[CLONE_INDEXER]: function(state, { payload }) {
|
||||
const id = payload.id;
|
||||
const newState = getSectionState(state, section);
|
||||
|
||||
@@ -2,13 +2,17 @@ import { createSelector } from 'reselect';
|
||||
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
|
||||
export default function createEnabledDownloadClientsSelector(
|
||||
protocol: DownloadProtocol
|
||||
) {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.downloadClients', sortByName),
|
||||
createSortedSectionSelector<DownloadClient>(
|
||||
'settings.downloadClients',
|
||||
sortByProp('name')
|
||||
),
|
||||
(downloadClients: DownloadClientAppState) => {
|
||||
const { isFetching, isPopulated, error, items } = downloadClients;
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
|
||||
function createSortedSectionSelector(section, comparer) {
|
||||
function createSortedSectionSelector<T>(
|
||||
section: string,
|
||||
comparer: (a: T, b: T) => number
|
||||
) {
|
||||
return createSelector(
|
||||
(state) => state,
|
||||
(state) => {
|
||||
const sectionState = getSectionState(state, section, true);
|
||||
|
||||
return {
|
||||
...sectionState,
|
||||
items: [...sectionState.items].sort(comparer)
|
||||
items: [...sectionState.items].sort(comparer),
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
function sortByName(a, b) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
export default sortByName;
|
||||
13
frontend/src/Utilities/Array/sortByProp.ts
Normal file
13
frontend/src/Utilities/Array/sortByProp.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { StringKey } from 'typings/Helpers/KeysMatching';
|
||||
|
||||
export function sortByProp<
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
T extends Record<K, string>,
|
||||
K extends StringKey<T>
|
||||
>(sortKey: K) {
|
||||
return (a: T, b: T) => {
|
||||
return a[sortKey].localeCompare(b[sortKey], undefined, { numeric: true });
|
||||
};
|
||||
}
|
||||
|
||||
export default sortByProp;
|
||||
7
frontend/src/typings/Helpers/KeysMatching.ts
Normal file
7
frontend/src/typings/Helpers/KeysMatching.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
type KeysMatching<T, V> = {
|
||||
[K in keyof T]-?: T[K] extends V ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type StringKey<T> = KeysMatching<T, string>;
|
||||
|
||||
export default KeysMatching;
|
||||
@@ -2,6 +2,7 @@ export interface IndexerStatsIndexer {
|
||||
indexerId: number;
|
||||
indexerName: string;
|
||||
averageResponseTime: number;
|
||||
averageGrabResponseTime: number;
|
||||
numberOfQueries: number;
|
||||
numberOfGrabs: number;
|
||||
numberOfRssQueries: number;
|
||||
|
||||
28
package.json
28
package.json
@@ -23,11 +23,11 @@
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@fortawesome/fontawesome-free": "6.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.100.0",
|
||||
@@ -35,7 +35,7 @@
|
||||
"@types/node": "18.19.31",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"chart.js": "4.4.2",
|
||||
"chart.js": "4.4.3",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
"connected-react-router": "6.9.3",
|
||||
@@ -44,7 +44,7 @@
|
||||
"history": "4.10.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.7.0",
|
||||
"jquery": "3.7.1",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.4",
|
||||
@@ -86,13 +86,13 @@
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/eslint-parser": "7.24.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||
"@babel/core": "7.24.9",
|
||||
"@babel/eslint-parser": "7.24.8",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@babel/preset-env": "7.24.8",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@types/lodash": "4.14.194",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-text-truncate": "0.14.1",
|
||||
@@ -105,7 +105,7 @@
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.37.0",
|
||||
"core-js": "3.37.1",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.57.0",
|
||||
|
||||
@@ -7,6 +7,7 @@ using NzbDrone.Core.HealthCheck.Checks;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
{
|
||||
@@ -21,28 +22,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
.Returns("Some Warning Message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_error_when_app_folder_is_write_protected()
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
Mocker.GetMock<IAppFolderInfo>()
|
||||
.Setup(s => s.StartUpFolder)
|
||||
.Returns(@"C:\NzbDrone");
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(c => c.FolderWritable(It.IsAny<string>()))
|
||||
.Returns(false);
|
||||
|
||||
Subject.Check().ShouldBeError();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled()
|
||||
{
|
||||
PosixOnly();
|
||||
|
||||
const string startupFolder = @"/opt/nzbdrone";
|
||||
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>()
|
||||
.Setup(s => s.UpdateAutomatically)
|
||||
@@ -62,10 +45,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[Test]
|
||||
public void should_return_error_when_ui_folder_is_write_protected_and_update_automatically_is_enabled()
|
||||
{
|
||||
PosixOnly();
|
||||
|
||||
const string startupFolder = @"/opt/nzbdrone";
|
||||
const string uiFolder = @"/opt/nzbdrone/UI";
|
||||
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||
var uiFolder = @"C:\NzbDrone\UI".AsOsAgnostic();
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>()
|
||||
.Setup(s => s.UpdateAutomatically)
|
||||
@@ -89,7 +70,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[Test]
|
||||
public void should_not_return_error_when_app_folder_is_write_protected_and_external_script_enabled()
|
||||
{
|
||||
PosixOnly();
|
||||
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>()
|
||||
.Setup(s => s.UpdateAutomatically)
|
||||
@@ -101,7 +82,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
|
||||
Mocker.GetMock<IAppFolderInfo>()
|
||||
.Setup(s => s.StartUpFolder)
|
||||
.Returns(@"/opt/nzbdrone");
|
||||
.Returns(startupFolder);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Verify(c => c.FolderWritable(It.IsAny<string>()), Times.Never());
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
public IEnumerable<int> AnimeSyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Sync Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Sync also searching for anime using the standard numbering", Advanced = true)]
|
||||
public bool SyncAnimeStandardFormatSearch { get; set; }
|
||||
public bool SyncAnimeStandardFormatSearch { get; set; } = true;
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
@@ -273,7 +273,7 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", false, false);
|
||||
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false);
|
||||
|
||||
public UpdateMechanism UpdateMechanism =>
|
||||
Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue)
|
||||
|
||||
@@ -219,7 +219,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
|
||||
protected virtual IList<TableDefinition> ReadTables()
|
||||
{
|
||||
const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name;";
|
||||
const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name;";
|
||||
var dtTable = Read(sqlCommand).Tables[0];
|
||||
|
||||
var tableDefinitionList = new List<TableDefinition>();
|
||||
|
||||
@@ -39,9 +39,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
|
||||
|
||||
_logger.Debug("Downloading NZB from: {0} to: {1}", url, nzbFile);
|
||||
|
||||
var nzbData = await indexer.Download(url);
|
||||
var downloadResponse = await indexer.Download(url);
|
||||
|
||||
File.WriteAllBytes(nzbFile, nzbData);
|
||||
await File.WriteAllBytesAsync(nzbFile, downloadResponse.Data);
|
||||
|
||||
_logger.Debug("NZB Download succeeded, saved to: {0}", nzbFile);
|
||||
|
||||
|
||||
@@ -148,9 +148,11 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
try
|
||||
{
|
||||
downloadedBytes = await indexer.Download(url);
|
||||
var downloadResponse = await indexer.Download(url);
|
||||
downloadedBytes = downloadResponse.Data;
|
||||
_indexerStatusService.RecordSuccess(indexerId);
|
||||
grabEvent.Successful = true;
|
||||
grabEvent.ElapsedTime = downloadResponse.ElapsedTime;
|
||||
}
|
||||
catch (ReleaseUnavailableException)
|
||||
{
|
||||
|
||||
@@ -127,9 +127,8 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
private async Task<string> DownloadFromWebUrl(TorrentInfo release, IIndexer indexer, string torrentUrl)
|
||||
{
|
||||
byte[] torrentFile = null;
|
||||
|
||||
torrentFile = await indexer.Download(new Uri(torrentUrl));
|
||||
var downloadResponse = await indexer.Download(new Uri(torrentUrl));
|
||||
var torrentFile = downloadResponse.Data;
|
||||
|
||||
// handle magnet URLs
|
||||
if (torrentFile.Length >= 7
|
||||
|
||||
@@ -41,12 +41,10 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
var filename = StringUtil.CleanFileName(release.Title) + ".nzb";
|
||||
|
||||
byte[] nzbData;
|
||||
|
||||
nzbData = await indexer.Download(url);
|
||||
var downloadResponse = await indexer.Download(url);
|
||||
|
||||
_logger.Info("Adding report [{0}] to the queue.", release.Title);
|
||||
return AddFromNzbFile(release, filename, nzbData);
|
||||
return AddFromNzbFile(release, filename, downloadResponse.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
var startupFolder = _appFolderInfo.StartUpFolder;
|
||||
var uiFolder = Path.Combine(startupFolder, "UI");
|
||||
|
||||
if ((OsInfo.IsWindows || _configFileProvider.UpdateAutomatically) &&
|
||||
if (_configFileProvider.UpdateAutomatically &&
|
||||
_configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn &&
|
||||
!_osInfo.IsDocker)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers;
|
||||
@@ -203,9 +204,29 @@ namespace NzbDrone.Core.History
|
||||
history.Data.Add("Host", message.Host ?? string.Empty);
|
||||
history.Data.Add("GrabMethod", message.Redirect ? "Redirect" : "Proxy");
|
||||
history.Data.Add("GrabTitle", message.Title);
|
||||
history.Data.Add("Categories", string.Join(",", message.Release.Categories.Select(x => x.Id) ?? Array.Empty<int>()));
|
||||
history.Data.Add("Url", message.Url ?? string.Empty);
|
||||
|
||||
if (message.ElapsedTime > 0)
|
||||
{
|
||||
history.Data.Add("ElapsedTime", message.ElapsedTime.ToString());
|
||||
}
|
||||
|
||||
if (message.Release.InfoUrl.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
history.Data.Add("InfoUrl", message.Release.InfoUrl);
|
||||
}
|
||||
|
||||
if (message.DownloadClient.IsNotNullOrWhiteSpace() || message.DownloadClientName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
history.Data.Add("DownloadClient", message.DownloadClient ?? string.Empty);
|
||||
history.Data.Add("DownloadClientName", message.DownloadClientName ?? string.Empty);
|
||||
}
|
||||
|
||||
if (message.Release.PublishDate != DateTime.MinValue)
|
||||
{
|
||||
history.Data.Add("PublishedDate", message.Release.PublishDate.ToString("s") + "Z");
|
||||
}
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
|
||||
@@ -219,7 +240,7 @@ namespace NzbDrone.Core.History
|
||||
Successful = message.Successful
|
||||
};
|
||||
|
||||
history.Data.Add("ElapsedTime", message.Time.ToString());
|
||||
history.Data.Add("ElapsedTime", message.ElapsedTime.ToString());
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
public class TrimLogDatabase : IHousekeepingTask
|
||||
{
|
||||
private readonly ILogRepository _logRepo;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public TrimLogDatabase(ILogRepository logRepo)
|
||||
public TrimLogDatabase(ILogRepository logRepo, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_logRepo = logRepo;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
if (!_configFileProvider.LogDbEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logRepo.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
string episodeString;
|
||||
if (DateTime.TryParseExact($"{Season} {Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
episodeString = showDate.ToString("yyyy.MM.dd");
|
||||
episodeString = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (Episode.IsNullOrWhiteSpace())
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.IndexerStats
|
||||
public int IndexerId { get; set; }
|
||||
public string IndexerName { get; set; }
|
||||
public int AverageResponseTime { get; set; }
|
||||
public int AverageGrabResponseTime { get; set; }
|
||||
public int NumberOfQueries { get; set; }
|
||||
public int NumberOfGrabs { get; set; }
|
||||
public int NumberOfRssQueries { get; set; }
|
||||
|
||||
@@ -61,13 +61,8 @@ namespace NzbDrone.Core.IndexerStats
|
||||
.ThenBy(v => v.Id)
|
||||
.ToArray();
|
||||
|
||||
var temp = 0;
|
||||
var elapsedTimeEvents = sortedEvents
|
||||
.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp) && h.Data.GetValueOrDefault("cached") != "1")
|
||||
.Select(_ => temp)
|
||||
.ToArray();
|
||||
|
||||
indexerStats.AverageResponseTime = elapsedTimeEvents.Any() ? (int)elapsedTimeEvents.Average() : 0;
|
||||
indexerStats.AverageResponseTime = CalculateAverageElapsedTime(sortedEvents.Where(h => h.EventType is HistoryEventType.IndexerRss or HistoryEventType.IndexerQuery).ToArray());
|
||||
indexerStats.AverageGrabResponseTime = CalculateAverageElapsedTime(sortedEvents.Where(h => h.EventType is HistoryEventType.ReleaseGrabbed).ToArray());
|
||||
|
||||
foreach (var historyEvent in sortedEvents)
|
||||
{
|
||||
@@ -176,5 +171,17 @@ namespace NzbDrone.Core.IndexerStats
|
||||
HostStatistics = hostStatsList
|
||||
};
|
||||
}
|
||||
|
||||
private static int CalculateAverageElapsedTime(History.History[] sortedEvents)
|
||||
{
|
||||
var temp = 0;
|
||||
|
||||
var elapsedTimeEvents = sortedEvents
|
||||
.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp) && temp > 0 && h.Data.GetValueOrDefault("cached") != "1")
|
||||
.Select(_ => temp)
|
||||
.ToArray();
|
||||
|
||||
return elapsedTimeEvents.Any() ? (int)elapsedTimeEvents.Average() : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,16 +644,16 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private static int? ParseSeasonFromTitles(IReadOnlyCollection<string> titles)
|
||||
{
|
||||
var advancedSeasonRegex = new Regex(@"(\d+)(st|nd|rd|th) Season", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
var advancedSeasonRegex = new Regex(@"\b(?:(?<season>\d+)(?:st|nd|rd|th) Season|Season (?<season>\d+))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
var seasonCharactersRegex = new Regex(@"(I{2,})$", RegexOptions.Compiled);
|
||||
var seasonNumberRegex = new Regex(@"\b(?:S)?([2-9])$", RegexOptions.Compiled);
|
||||
var seasonNumberRegex = new Regex(@"\b(?<!Part[- ._])(?:S)?(?<season>[2-9])$", RegexOptions.Compiled);
|
||||
|
||||
foreach (var title in titles)
|
||||
{
|
||||
var advancedSeasonRegexMatch = advancedSeasonRegex.Match(title);
|
||||
if (advancedSeasonRegexMatch.Success)
|
||||
{
|
||||
return ParseUtil.CoerceInt(advancedSeasonRegexMatch.Groups[1].Value);
|
||||
return ParseUtil.CoerceInt(advancedSeasonRegexMatch.Groups["season"].Value);
|
||||
}
|
||||
|
||||
var seasonCharactersRegexMatch = seasonCharactersRegex.Match(title);
|
||||
@@ -665,7 +665,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var seasonNumberRegexMatch = seasonNumberRegex.Match(title);
|
||||
if (seasonNumberRegexMatch.Success)
|
||||
{
|
||||
return ParseUtil.CoerceInt(seasonNumberRegexMatch.Groups[1].Value);
|
||||
return ParseUtil.CoerceInt(seasonNumberRegexMatch.Groups["season"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return new BakaBTParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
var request = new HttpRequestBuilder(link.ToString())
|
||||
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
|
||||
|
||||
@@ -267,10 +267,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var details = row.InfoUrl;
|
||||
var link = row.DownloadLink;
|
||||
|
||||
// BHD can return crazy values for tmdb
|
||||
var tmdbId = row.TmdbId.IsNullOrWhiteSpace() ? 0 : ParseUtil.TryCoerceInt(row.TmdbId.Split("/")[1], out var tmdbResult) ? tmdbResult : 0;
|
||||
var imdbId = ParseUtil.GetImdbId(row.ImdbId).GetValueOrDefault();
|
||||
|
||||
var flags = new HashSet<IndexerFlag>();
|
||||
|
||||
if (row.Internal)
|
||||
@@ -291,8 +287,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Size = row.Size,
|
||||
Grabs = row.Grabs,
|
||||
Seeders = row.Seeders,
|
||||
ImdbId = imdbId,
|
||||
TmdbId = tmdbId,
|
||||
ImdbId = ParseUtil.GetImdbId(row.ImdbId).GetValueOrDefault(),
|
||||
Peers = row.Leechers + row.Seeders,
|
||||
DownloadVolumeFactor = row.Freeleech || row.Limited ? 0 : row.Promo75 ? 0.25 : row.Promo50 ? 0.5 : row.Promo25 ? 0.75 : 1,
|
||||
UploadVolumeFactor = 1,
|
||||
@@ -300,6 +295,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
MinimumSeedTime = 172800, // 120 hours
|
||||
};
|
||||
|
||||
// BHD can return crazy values for tmdb
|
||||
if (row.TmdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var tmdbId = row.TmdbId.Split("/").ElementAtOrDefault(1);
|
||||
release.TmdbId = tmdbId != null && ParseUtil.TryCoerceInt(tmdbId, out var tmdbResult) ? tmdbResult : 0;
|
||||
}
|
||||
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
else if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
// Daily Episode
|
||||
parameters.Name = showDate.ToString("yyyy.MM.dd");
|
||||
parameters.Name = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture);
|
||||
parameters.Category = "Episode";
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
}
|
||||
|
||||
@@ -82,9 +82,10 @@ public abstract class GazelleBase<TSettings> : TorrentIndexerBase<TSettings>
|
||||
|
||||
protected virtual bool CheckForLoginError(HttpResponse response) => true;
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
var response = await base.Download(link);
|
||||
var downloadResponse = await base.Download(link);
|
||||
var response = downloadResponse.Data;
|
||||
|
||||
if (response.Length >= 1
|
||||
&& response[0] != 'd' // simple test for torrent vs HTML content
|
||||
@@ -99,11 +100,11 @@ public abstract class GazelleBase<TSettings> : TorrentIndexerBase<TSettings>
|
||||
// download again without usetoken=1
|
||||
var requestLinkNew = link.ToString().Replace("&usetoken=1", "");
|
||||
|
||||
response = await base.Download(new Uri(requestLinkNew));
|
||||
downloadResponse = await base.Download(new Uri(requestLinkNew));
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
return downloadResponse;
|
||||
}
|
||||
|
||||
protected override IDictionary<string, string> GetCookies()
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
|
||||
if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
query.Search = showDate.ToString("yyyy-MM-dd");
|
||||
query.Search = showDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -50,28 +50,33 @@ namespace NzbDrone.Core.Indexers.Headphones
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri);
|
||||
|
||||
var downloadBytes = Array.Empty<byte>();
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
|
||||
request.Credentials = new BasicNetworkCredential(Settings.Username, Settings.Password);
|
||||
|
||||
byte[] downloadBytes;
|
||||
long elapsedTime;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
downloadBytes = response.ResponseData;
|
||||
elapsedTime = response.ElapsedTime;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id);
|
||||
_logger.Error("Download failed");
|
||||
throw;
|
||||
}
|
||||
|
||||
return downloadBytes;
|
||||
ValidateDownloadData(downloadBytes);
|
||||
|
||||
return new IndexerDownloadResponse(downloadBytes, elapsedTime);
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
|
||||
@@ -59,7 +59,7 @@ public class MTeamTp : TorrentIndexerBase<MTeamTpSettings>
|
||||
return new MTeamTpParser(Settings, Capabilities.Categories, BuildApiUrl(Settings));
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
var request = new HttpRequestBuilder(link.ToString())
|
||||
.SetHeader("x-api-key", Settings.ApiKey)
|
||||
@@ -338,6 +338,13 @@ public class MTeamTpParser : IParseIndexerResponse
|
||||
|
||||
if (jsonResponse?.Data?.Torrents == null)
|
||||
{
|
||||
if (jsonResponse != null &&
|
||||
jsonResponse.Message.IsNotNullOrWhiteSpace() &&
|
||||
jsonResponse.Message.ToUpperInvariant() != "SUCCESS")
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Invalid response received from M-Team. Response from API: {jsonResponse.Message}");
|
||||
}
|
||||
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
@@ -464,6 +471,7 @@ internal class MTeamTpApiSearchQuery
|
||||
internal class MTeamTpApiResponse
|
||||
{
|
||||
public MTeamTpApiData Data { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
internal class MTeamTpApiData
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return new MyAnonamouseParser(Settings, Capabilities.Categories, _httpClient, _cacheManager, _logger);
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
if (Settings.Freeleech)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
@@ -42,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new NebulanceRequestGenerator(Settings);
|
||||
return new NebulanceRequestGenerator(Settings, _logger);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
@@ -68,26 +67,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return Task.FromResult(request);
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
|
||||
|
||||
return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList();
|
||||
}
|
||||
|
||||
protected override IEnumerable<ReleaseInfo> FilterReleasesByQuery(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (!searchCriteria.IsRssSearch &&
|
||||
searchCriteria.IsIdSearch &&
|
||||
searchCriteria is TvSearchCriteria tvSearchCriteria &&
|
||||
tvSearchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
releases = releases.Where(r => r.Title.IsNotNullOrWhiteSpace() && r.Title.ContainsIgnoreCase(tvSearchCriteria.EpisodeSearchString)).ToList();
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
@@ -111,10 +90,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class NebulanceRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly NebulanceSettings _settings;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NebulanceRequestGenerator(NebulanceSettings settings)
|
||||
public NebulanceRequestGenerator(NebulanceSettings settings, Logger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
@@ -136,40 +117,66 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Age = ">0"
|
||||
};
|
||||
|
||||
if (searchCriteria.SanitizedTvSearchString.IsNotNullOrWhiteSpace())
|
||||
if (searchCriteria.TvMazeId is > 0)
|
||||
{
|
||||
queryParams.Name = "%" + Regex.Replace(searchCriteria.SanitizedTvSearchString, "[\\W]+", "%").Trim() + "%";
|
||||
queryParams.TvMaze = searchCriteria.TvMazeId.Value;
|
||||
}
|
||||
else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Imdb = searchCriteria.FullImdbId;
|
||||
}
|
||||
|
||||
if (searchCriteria.TvMazeId.HasValue)
|
||||
{
|
||||
queryParams.Tvmaze = searchCriteria.TvMazeId.Value;
|
||||
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
|
||||
|
||||
if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
|
||||
if (searchQuery.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Release = searchQuery;
|
||||
}
|
||||
|
||||
if (searchCriteria.Season.HasValue &&
|
||||
searchCriteria.Episode.IsNotNullOrWhiteSpace() &&
|
||||
DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
if (searchQuery.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Name = "%" + Regex.Replace(searchCriteria.EpisodeSearchString, "[\\W]+", "%").Trim() + "%";
|
||||
queryParams.Name = searchQuery;
|
||||
}
|
||||
|
||||
queryParams.Release = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (searchCriteria.Season.HasValue)
|
||||
{
|
||||
queryParams.Season = searchCriteria.Season.Value;
|
||||
}
|
||||
|
||||
if (searchCriteria.Episode.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.Episode, out var episodeNumber))
|
||||
{
|
||||
queryParams.Episode = episodeNumber;
|
||||
}
|
||||
}
|
||||
else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.ImdbId, out var intImdb))
|
||||
{
|
||||
queryParams.Imdb = intImdb;
|
||||
|
||||
if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Name = "%" + Regex.Replace(searchCriteria.EpisodeSearchString, "[\\W]+", "%").Trim() + "%";
|
||||
}
|
||||
if ((queryParams.Season.HasValue || queryParams.Episode.HasValue) &&
|
||||
queryParams.Name.IsNullOrWhiteSpace() &&
|
||||
queryParams.Release.IsNullOrWhiteSpace() &&
|
||||
!queryParams.TvMaze.HasValue &&
|
||||
queryParams.Imdb.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Debug("NBL API does not support season calls without name, series, id, imdb, tvmaze, or time keys.");
|
||||
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
if (queryParams.Name is { Length: > 0 and < 3 } || queryParams.Release is { Length: > 0 and < 3 })
|
||||
{
|
||||
_logger.Debug("NBL API does not support release calls that are 2 characters or fewer.");
|
||||
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
||||
|
||||
if (queryParams.Name.IsNotNullOrWhiteSpace() && (queryParams.Tvmaze is > 0 || queryParams.Imdb is > 0))
|
||||
{
|
||||
queryParams = queryParams.Clone();
|
||||
queryParams.Name = null;
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
||||
}
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
@@ -187,9 +194,18 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Age = ">0"
|
||||
};
|
||||
|
||||
if (searchCriteria.SanitizedSearchTerm.IsNotNullOrWhiteSpace())
|
||||
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
|
||||
|
||||
if (searchQuery.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Name = "%" + Regex.Replace(searchCriteria.SanitizedSearchTerm, "[\\W]+", "%").Trim() + "%";
|
||||
queryParams.Release = searchQuery;
|
||||
}
|
||||
|
||||
if (queryParams.Release is { Length: > 0 and < 3 })
|
||||
{
|
||||
_logger.Debug("NBL API does not support release calls that are 2 characters or fewer.");
|
||||
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
||||
@@ -231,11 +247,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request", indexerResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
JsonRpcResponse<NebulanceTorrents> jsonResponse;
|
||||
JsonRpcResponse<NebulanceResponse> jsonResponse;
|
||||
|
||||
try
|
||||
{
|
||||
jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse.Content);
|
||||
jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceResponse>>(indexerResponse.HttpResponse.Content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -249,7 +265,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error);
|
||||
}
|
||||
|
||||
if (jsonResponse.Result.Items.Count == 0)
|
||||
if (jsonResponse.Result?.Items == null || jsonResponse.Result.Items.Count == 0)
|
||||
{
|
||||
return torrentInfos;
|
||||
}
|
||||
@@ -264,14 +280,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = title,
|
||||
Guid = details,
|
||||
InfoUrl = details,
|
||||
PosterUrl = row.Banner,
|
||||
DownloadUrl = row.Download,
|
||||
Title = title.Trim(),
|
||||
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) },
|
||||
Size = ParseUtil.CoerceLong(row.Size),
|
||||
Files = row.FileList.Length,
|
||||
Files = row.FileList.Count(),
|
||||
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
|
||||
Grabs = ParseUtil.CoerceInt(row.Snatch),
|
||||
Seeders = ParseUtil.CoerceInt(row.Seed),
|
||||
@@ -280,7 +295,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
MinimumRatio = 0, // ratioless
|
||||
MinimumSeedTime = row.Category.ToLower() == "season" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
|
||||
DownloadVolumeFactor = 0, // ratioless tracker
|
||||
UploadVolumeFactor = 1
|
||||
UploadVolumeFactor = 1,
|
||||
PosterUrl = row.Banner
|
||||
};
|
||||
|
||||
if (row.TvMazeId.IsNotNullOrWhiteSpace())
|
||||
@@ -312,60 +328,86 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Time { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="age", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Age { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="tvmaze", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Tvmaze { get; set; }
|
||||
public int? TvMaze { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="imdb", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Imdb { get; set; }
|
||||
public string Imdb { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Hash { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string[] Tags { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="name", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="release", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Release { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Category { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Series { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="season", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Season { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="episode", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Episode { get; set; }
|
||||
|
||||
public NebulanceQuery Clone()
|
||||
{
|
||||
return MemberwiseClone() as NebulanceQuery;
|
||||
}
|
||||
}
|
||||
|
||||
public class NebulanceResponse
|
||||
{
|
||||
public List<NebulanceTorrent> Items { get; set; }
|
||||
}
|
||||
|
||||
public class NebulanceTorrent
|
||||
{
|
||||
[JsonPropertyName("rls_name")]
|
||||
public string ReleaseTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("cat")]
|
||||
public string Category { get; set; }
|
||||
|
||||
public string Size { get; set; }
|
||||
public string Seed { get; set; }
|
||||
public string Leech { get; set; }
|
||||
public string Snatch { get; set; }
|
||||
public string Download { get; set; }
|
||||
|
||||
[JsonPropertyName("file_list")]
|
||||
public string[] FileList { get; set; }
|
||||
public IEnumerable<string> FileList { get; set; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("group_name")]
|
||||
public string GroupName { get; set; }
|
||||
|
||||
[JsonPropertyName("series_banner")]
|
||||
public string Banner { get; set; }
|
||||
|
||||
[JsonPropertyName("group_id")]
|
||||
public string TorrentId { get; set; }
|
||||
|
||||
[JsonPropertyName("series_id")]
|
||||
public string TvMazeId { get; set; }
|
||||
|
||||
[JsonPropertyName("rls_utc")]
|
||||
public string PublishDateUtc { get; set; }
|
||||
public IEnumerable<string> Tags { get; set; }
|
||||
}
|
||||
|
||||
public class NebulanceTorrents
|
||||
{
|
||||
public List<NebulanceTorrent> Items { get; set; }
|
||||
public int Results { get; set; }
|
||||
public IEnumerable<string> Tags { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,18 +89,20 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return caps;
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
var request = new HttpRequestBuilder(link.AbsoluteUri)
|
||||
.SetHeader("Authorization", $"token {Settings.Apikey}")
|
||||
.Build();
|
||||
|
||||
var downloadBytes = Array.Empty<byte>();
|
||||
byte[] downloadBytes;
|
||||
long elapsedTime;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
downloadBytes = response.ResponseData;
|
||||
elapsedTime = response.ElapsedTime;
|
||||
|
||||
if (downloadBytes.Length >= 1
|
||||
&& downloadBytes[0] != 'd' // simple test for torrent vs HTML content
|
||||
@@ -124,11 +126,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id);
|
||||
_logger.Error("Download failed");
|
||||
throw;
|
||||
}
|
||||
|
||||
ValidateDownloadData(downloadBytes);
|
||||
|
||||
return downloadBytes;
|
||||
return new IndexerDownloadResponse(downloadBytes, elapsedTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return new RuTrackerParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
if (Settings.UseMagnetLinks && link.PathAndQuery.Contains("viewtopic.php?t="))
|
||||
{
|
||||
|
||||
@@ -75,6 +75,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class SubsPleaseRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private static readonly Regex ResolutionRegex = new (@"\d{3,4}p", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private readonly NoAuthTorrentBaseSettings _settings;
|
||||
|
||||
public SubsPleaseRequestGenerator(NoAuthTorrentBaseSettings settings)
|
||||
@@ -134,15 +136,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private IEnumerable<IndexerRequest> GetSearchRequests(string term, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var searchTerm = Regex.Replace(term, "\\[?SubsPlease\\]?\\s*", string.Empty, RegexOptions.IgnoreCase).Trim();
|
||||
|
||||
// If the search terms contain a resolution, remove it from the query sent to the API
|
||||
var resMatch = Regex.Match(searchTerm, "\\d{3,4}[p|P]");
|
||||
if (resMatch.Success)
|
||||
{
|
||||
searchTerm = searchTerm.Replace(resMatch.Value, string.Empty).Trim();
|
||||
}
|
||||
|
||||
var queryParameters = new NameValueCollection
|
||||
{
|
||||
{ "tz", "UTC" }
|
||||
@@ -154,6 +147,16 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
else
|
||||
{
|
||||
var searchTerm = Regex.Replace(term, "\\[?SubsPlease\\]?\\s*", string.Empty, RegexOptions.IgnoreCase).Trim();
|
||||
|
||||
// If the search terms contain a resolution, remove it from the query sent to the API
|
||||
var resolutionMatch = ResolutionRegex.Match(searchTerm);
|
||||
|
||||
if (resolutionMatch.Success)
|
||||
{
|
||||
searchTerm = searchTerm.Replace(resolutionMatch.Value, string.Empty).Trim();
|
||||
}
|
||||
|
||||
queryParameters.Set("f", "search");
|
||||
queryParameters.Set("s", searchTerm);
|
||||
}
|
||||
@@ -201,7 +204,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
InfoUrl = _settings.BaseUrl + $"shows/{value.Page}/",
|
||||
InfoUrl = $"{_settings.BaseUrl}shows/{value.Page}/",
|
||||
PublishDate = value.ReleaseDate.LocalDateTime,
|
||||
Files = 1,
|
||||
Categories = new List<IndexerCategory> { NewznabStandardCategory.TVAnime },
|
||||
@@ -213,13 +216,18 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
UploadVolumeFactor = 1
|
||||
};
|
||||
|
||||
if (value.ImageUrl.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
release.PosterUrl = _settings.BaseUrl + value.ImageUrl.TrimStart('/');
|
||||
}
|
||||
|
||||
if (value.Episode.ToLowerInvariant() == "movie")
|
||||
{
|
||||
release.Categories.Add(NewznabStandardCategory.MoviesOther);
|
||||
}
|
||||
|
||||
// Ex: [SubsPlease] Shingeki no Kyojin (The Final Season) - 64 (1080p)
|
||||
release.Title += $"[SubsPlease] {value.Show} - {value.Episode} ({d.Resolution}p)";
|
||||
release.Title = $"[SubsPlease] {value.Show} - {value.Episode} ({d.Resolution}p)";
|
||||
release.MagnetUrl = d.Magnet;
|
||||
release.DownloadUrl = null;
|
||||
release.Guid = d.Magnet;
|
||||
@@ -269,6 +277,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public string Episode { get; set; }
|
||||
public SubPleaseDownloadInfo[] Downloads { get; set; }
|
||||
public string Xdcc { get; set; }
|
||||
|
||||
[JsonProperty("image_url")]
|
||||
public string ImageUrl { get; set; }
|
||||
public string Page { get; set; }
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public class Uniotaku : TorrentIndexerBase<UniotakuSettings>
|
||||
return !httpResponse.GetCookies().ContainsKey("uid") || !httpResponse.GetCookies().ContainsKey("pass");
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
var request = new HttpRequestBuilder(link.ToString())
|
||||
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
|
||||
|
||||
@@ -6,13 +6,13 @@ namespace NzbDrone.Core.Indexers.Events
|
||||
{
|
||||
public int IndexerId { get; set; }
|
||||
public bool Successful { get; set; }
|
||||
public long Time { get; set; }
|
||||
public long ElapsedTime { get; set; }
|
||||
|
||||
public IndexerAuthEvent(int indexerId, bool successful, long time)
|
||||
public IndexerAuthEvent(int indexerId, bool successful, long elapsedTime)
|
||||
{
|
||||
IndexerId = indexerId;
|
||||
Successful = successful;
|
||||
Time = time;
|
||||
ElapsedTime = elapsedTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace NzbDrone.Core.Indexers.Events
|
||||
public string DownloadId { get; set; }
|
||||
public IIndexer Indexer { get; set; }
|
||||
public GrabTrigger GrabTrigger { get; set; }
|
||||
public long ElapsedTime { get; set; }
|
||||
|
||||
public IndexerDownloadEvent(ReleaseInfo release, bool successful, string source, string host, string title, string url)
|
||||
{
|
||||
|
||||
@@ -224,7 +224,7 @@ namespace NzbDrone.Core.Indexers
|
||||
return FetchReleases(g => SetCookieFunctions(g).GetSearchRequests(searchCriteria), searchCriteria);
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
Cookies = GetCookies();
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace NzbDrone.Core.Indexers
|
||||
if (request.Url.Scheme == "magnet")
|
||||
{
|
||||
ValidateMagnet(request.Url.FullUri);
|
||||
return Encoding.UTF8.GetBytes(request.Url.FullUri);
|
||||
return new IndexerDownloadResponse(Encoding.UTF8.GetBytes(request.Url.FullUri));
|
||||
}
|
||||
|
||||
if (request.RateLimit < RateLimit)
|
||||
@@ -244,6 +244,7 @@ namespace NzbDrone.Core.Indexers
|
||||
request.AllowAutoRedirect = false;
|
||||
|
||||
byte[] fileData;
|
||||
long elapsedTime;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -283,6 +284,7 @@ namespace NzbDrone.Core.Indexers
|
||||
}
|
||||
|
||||
fileData = response.ResponseData;
|
||||
elapsedTime = response.ElapsedTime;
|
||||
|
||||
_logger.Debug("Downloaded for release finished ({0} bytes from {1})", fileData.Length, link.AbsoluteUri);
|
||||
}
|
||||
@@ -320,7 +322,7 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
ValidateDownloadData(fileData);
|
||||
|
||||
return fileData;
|
||||
return new IndexerDownloadResponse(fileData, elapsedTime);
|
||||
}
|
||||
|
||||
protected virtual Task<HttpRequest> GetDownloadRequest(Uri link)
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Indexers
|
||||
Task<IndexerPageableQueryResult> Fetch(BookSearchCriteria searchCriteria);
|
||||
Task<IndexerPageableQueryResult> Fetch(BasicSearchCriteria searchCriteria);
|
||||
|
||||
Task<byte[]> Download(Uri link);
|
||||
Task<IndexerDownloadResponse> Download(Uri link);
|
||||
bool IsObsolete();
|
||||
|
||||
IndexerCapabilities GetCapabilities();
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace NzbDrone.Core.Indexers
|
||||
public abstract Task<IndexerPageableQueryResult> Fetch(TvSearchCriteria searchCriteria);
|
||||
public abstract Task<IndexerPageableQueryResult> Fetch(BookSearchCriteria searchCriteria);
|
||||
public abstract Task<IndexerPageableQueryResult> Fetch(BasicSearchCriteria searchCriteria);
|
||||
public abstract Task<byte[]> Download(Uri link);
|
||||
public abstract Task<IndexerDownloadResponse> Download(Uri link);
|
||||
|
||||
public abstract IndexerCapabilities GetCapabilities();
|
||||
|
||||
|
||||
13
src/NzbDrone.Core/Indexers/IndexerDownloadResponse.cs
Normal file
13
src/NzbDrone.Core/Indexers/IndexerDownloadResponse.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace NzbDrone.Core.Indexers;
|
||||
|
||||
public class IndexerDownloadResponse
|
||||
{
|
||||
public byte[] Data { get; private set; }
|
||||
public long ElapsedTime { get; private set; }
|
||||
|
||||
public IndexerDownloadResponse(byte[] data, long elapsedTime = 0)
|
||||
{
|
||||
Data = data;
|
||||
ElapsedTime = elapsedTime;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ namespace NzbDrone.Core.Indexers
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.Info("Invalid torrent file contents: {0}", Encoding.ASCII.GetString(fileData));
|
||||
_logger.Debug("Invalid torrent file contents: {0}", Encoding.ASCII.GetString(fileData));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,5 +357,9 @@
|
||||
"CustomFilter": "مرشحات مخصصة",
|
||||
"IndexerHDBitsSettingsMediums": "متوسط",
|
||||
"GrabRelease": "انتزاع الإصدار",
|
||||
"ProxyValidationBadRequest": "فشل اختبار الوكيل. رمز الحالة: {statusCode}"
|
||||
"ProxyValidationBadRequest": "فشل اختبار الوكيل. رمز الحالة: {statusCode}",
|
||||
"Script": "النصي",
|
||||
"BuiltIn": "مدمج",
|
||||
"PublishedDate": "تاريخ النشر",
|
||||
"AllSearchResultsHiddenByFilter": "يتم إخفاء جميع النتائج بواسطة عامل التصفية المطبق"
|
||||
}
|
||||
|
||||
@@ -357,5 +357,9 @@
|
||||
"IndexerHDBitsSettingsMediums": "Среден",
|
||||
"CustomFilter": "Персонализирани филтри",
|
||||
"GrabRelease": "Grab Release",
|
||||
"ProxyValidationBadRequest": "Неуспешно тестване на прокси. Код на състоянието: {statusCode}"
|
||||
"ProxyValidationBadRequest": "Неуспешно тестване на прокси. Код на състоянието: {statusCode}",
|
||||
"BuiltIn": "Вграден",
|
||||
"Script": "Сценарий",
|
||||
"PublishedDate": "Дата на публикуване",
|
||||
"AllSearchResultsHiddenByFilter": "Всички резултати са скрити от приложения филтър"
|
||||
}
|
||||
|
||||
@@ -468,5 +468,21 @@
|
||||
"Stats": "Estadístiques",
|
||||
"Private": "Privat",
|
||||
"Proxies": "Servidors intermediaris",
|
||||
"Public": "Públic"
|
||||
"Public": "Públic",
|
||||
"DeleteSelectedIndexer": "Suprimeix els indexadors seleccionats",
|
||||
"EditSyncProfile": "Afegeix perfil de sincronització",
|
||||
"Menu": "Menú",
|
||||
"OnGrabHelpText": "Al capturar llançament",
|
||||
"ProxyValidationBadRequest": "No s'ha pogut provar el servidor intermediari. Codi d'estat: {statusCode}",
|
||||
"Default": "Per defecte",
|
||||
"GrabRelease": "Captura novetat",
|
||||
"ManualGrab": "Captura manual",
|
||||
"PrioritySettings": "Prioritat: {priority}",
|
||||
"Any": "Qualsevol",
|
||||
"BuiltIn": "Integrat",
|
||||
"Script": "Script",
|
||||
"InfoUrl": "URL d'informació",
|
||||
"PublishedDate": "Data de publicació",
|
||||
"Redirected": "Redirecció",
|
||||
"AllSearchResultsHiddenByFilter": "Tots els resultats estan ocults pel filtre aplicat"
|
||||
}
|
||||
|
||||
@@ -415,5 +415,13 @@
|
||||
"CustomFilter": "Vlastní filtry",
|
||||
"ProxyValidationBadRequest": "Nepodařilo se otestovat proxy. StatusCode: {statusCode}",
|
||||
"Default": "Výchozí",
|
||||
"GrabRelease": "Uchopte uvolnění"
|
||||
"GrabRelease": "Uchopte uvolnění",
|
||||
"Category": "Kategorie",
|
||||
"BlackholeFolderHelpText": "Složka do které {appName} uloží {extension} soubor",
|
||||
"DownloadClientSettingsUrlBaseHelpText": "Přidá předponu do {connectionName} url, jako např. {url}",
|
||||
"Any": "Jakákoliv",
|
||||
"BuiltIn": "Vestavěný",
|
||||
"Script": "Skript",
|
||||
"PublishedDate": "Datum zveřejnění",
|
||||
"AllSearchResultsHiddenByFilter": "Všechny výsledky jsou schovány použitým filtrem"
|
||||
}
|
||||
|
||||
@@ -394,5 +394,9 @@
|
||||
"IndexerHDBitsSettingsMediums": "Medium",
|
||||
"CustomFilter": "Bruger Tilpassede Filtere",
|
||||
"ProxyValidationBadRequest": "Kunne ikke teste proxy. Statuskode: {statusCode}",
|
||||
"GrabRelease": "Grab Release"
|
||||
"GrabRelease": "Grab Release",
|
||||
"Script": "Manuskript",
|
||||
"BuiltIn": "Indbygget",
|
||||
"PublishedDate": "Udgivelsesdato",
|
||||
"AllSearchResultsHiddenByFilter": "Alle resultater skjules af det anvendte filter"
|
||||
}
|
||||
|
||||
@@ -605,5 +605,12 @@
|
||||
"TorrentBlackholeSaveMagnetFiles": "Speicher Magnetdateien",
|
||||
"TorrentBlackholeSaveMagnetFilesExtension": "Speicher die Magnet-Dateienerweiterung",
|
||||
"Default": "Standard",
|
||||
"GrabRelease": "Release erfassen"
|
||||
"GrabRelease": "Release erfassen",
|
||||
"Script": "Skript",
|
||||
"IndexerDownloadClientHealthCheckMessage": "Indexer mit ungültigen Downloader: {indexerNames}.",
|
||||
"Any": "Beliebig",
|
||||
"BuiltIn": "Eingebaut",
|
||||
"PublishedDate": "Veröffentlichungsdatum",
|
||||
"Redirected": "Umleiten",
|
||||
"AllSearchResultsHiddenByFilter": "Alle Ergebnisse werden durch den angewendeten Filter ausgeblendet"
|
||||
}
|
||||
|
||||
@@ -524,5 +524,10 @@
|
||||
"UseSsl": "Χρησιμοποιήστε SSL",
|
||||
"CustomFilter": "Custom Φιλτρα",
|
||||
"GrabRelease": "Πιάσε την απελευθέρωση",
|
||||
"ProxyValidationBadRequest": "Αποτυχία δοκιμής διακομιστή μεσολάβησης. StatusCode: {statusCode}"
|
||||
"ProxyValidationBadRequest": "Αποτυχία δοκιμής διακομιστή μεσολάβησης. StatusCode: {statusCode}",
|
||||
"Script": "Γραφή",
|
||||
"BuiltIn": "Ενσωματωμένο",
|
||||
"PublishedDate": "Ημερομηνία δημοσίευσης",
|
||||
"Redirected": "Διευθύνω πάλιν",
|
||||
"AllSearchResultsHiddenByFilter": "Όλα τα αποτελέσματα αποκρύπτονται από το εφαρμοσμένο φίλτρο"
|
||||
}
|
||||
|
||||
@@ -31,8 +31,10 @@
|
||||
"Album": "Album",
|
||||
"All": "All",
|
||||
"AllIndexersHiddenDueToFilter": "All indexers are hidden due to applied filter.",
|
||||
"AllSearchResultsHiddenByFilter": "All search results are hidden by the applied filter.",
|
||||
"Analytics": "Analytics",
|
||||
"AnalyticsEnabledHelpText": "Send anonymous usage and error information to {appName}'s servers. This includes information on your browser, which {appName} WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes.",
|
||||
"Any": "Any",
|
||||
"ApiKey": "API Key",
|
||||
"ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {length} characters long. You can do this via settings or the config file",
|
||||
"AppDataDirectory": "AppData Directory",
|
||||
@@ -87,6 +89,8 @@
|
||||
"Author": "Author",
|
||||
"Automatic": "Automatic",
|
||||
"AutomaticSearch": "Automatic Search",
|
||||
"AverageGrabs": "Average Grabs",
|
||||
"AverageQueries": "Average Queries",
|
||||
"AverageResponseTimesMs": "Average Indexer Response Times (ms)",
|
||||
"Backup": "Backup",
|
||||
"BackupFolderHelpText": "Relative paths will be under {appName}'s AppData directory",
|
||||
@@ -105,6 +109,7 @@
|
||||
"Branch": "Branch",
|
||||
"BranchUpdate": "Branch to use to update {appName}",
|
||||
"BranchUpdateMechanism": "Branch used by external update mechanism",
|
||||
"BuiltIn": "Built-In",
|
||||
"BypassProxyForLocalAddresses": "Bypass Proxy for Local Addresses",
|
||||
"Cancel": "Cancel",
|
||||
"CancelPendingTask": "Are you sure you want to cancel this pending task?",
|
||||
@@ -422,6 +427,7 @@
|
||||
"IndexerVipExpiringHealthCheckMessage": "Indexer VIP benefits expiring soon: {indexerNames}",
|
||||
"Indexers": "Indexers",
|
||||
"Info": "Info",
|
||||
"InfoUrl": "Info URL",
|
||||
"InitialFailure": "Initial Failure",
|
||||
"InstanceName": "Instance Name",
|
||||
"InstanceNameHelpText": "Instance name in tab and for Syslog app name",
|
||||
@@ -553,6 +559,7 @@
|
||||
"ProxyValidationBadRequest": "Failed to test proxy. Status code: {statusCode}",
|
||||
"ProxyValidationUnableToConnect": "Unable to connect to proxy: {exceptionMessage}. Check the log surrounding this error for details",
|
||||
"Public": "Public",
|
||||
"PublishedDate": "Published Date",
|
||||
"Publisher": "Publisher",
|
||||
"Query": "Query",
|
||||
"QueryOptions": "Query Options",
|
||||
@@ -566,6 +573,7 @@
|
||||
"Reddit": "Reddit",
|
||||
"Redirect": "Redirect",
|
||||
"RedirectHelpText": "Redirect incoming download request for indexer and pass the grab directly instead of proxying the request via {appName}",
|
||||
"Redirected": "Redirected",
|
||||
"Refresh": "Refresh",
|
||||
"RefreshMovie": "Refresh movie",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid {appName} release branch, you will not receive updates",
|
||||
@@ -601,6 +609,7 @@
|
||||
"SaveChanges": "Save Changes",
|
||||
"SaveSettings": "Save Settings",
|
||||
"Scheduled": "Scheduled",
|
||||
"Script": "Script",
|
||||
"ScriptPath": "Script Path",
|
||||
"Search": "Search",
|
||||
"SearchAllIndexers": "Search all indexers",
|
||||
|
||||
@@ -760,5 +760,14 @@
|
||||
"OverrideGrabModalTitle": "Sobrescribir y capturar - {title}",
|
||||
"PrioritySettings": "Prioridad: {priority}",
|
||||
"SelectDownloadClientModalTitle": "{modalTitle} - Seleccionar cliente de descarga",
|
||||
"Default": "Predeterminado"
|
||||
"Default": "Predeterminado",
|
||||
"BuiltIn": "Integrado",
|
||||
"Script": "Script",
|
||||
"Any": "Cualquiera",
|
||||
"Redirected": "Redirección",
|
||||
"InfoUrl": "Información de la URL",
|
||||
"PublishedDate": "Fecha de publicación",
|
||||
"AverageQueries": "Promedio de peticiones",
|
||||
"AverageGrabs": "Promedio de capturas",
|
||||
"AllSearchResultsHiddenByFilter": "Todos los resultados estan ocultos por el filtro aplicado"
|
||||
}
|
||||
|
||||
@@ -690,5 +690,12 @@
|
||||
"ProxyValidationBadRequest": "Välityspalvelintesti epäonnistui. Tilakoodi: {statusCode}.",
|
||||
"ProxyValidationUnableToConnect": "Tietolähdettä ei tavoiteta: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.",
|
||||
"ManualGrab": "Manuaalinen kaappaus",
|
||||
"OverrideAndAddToDownloadClient": "Ohita ja lisää latausjonoon"
|
||||
"OverrideAndAddToDownloadClient": "Ohita ja lisää latausjonoon",
|
||||
"BuiltIn": "Sisäänrakennettu",
|
||||
"Any": "Mikä vain",
|
||||
"Script": "Skripti",
|
||||
"InfoUrl": "Tietojen URL",
|
||||
"PublishedDate": "Julkaisupäivä",
|
||||
"Redirected": "Uudelleenohjaus",
|
||||
"AllSearchResultsHiddenByFilter": "Aktiivinen suodatin piilottaa kaikki tulokset."
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"Events": "Événements",
|
||||
"Edit": "Modifier",
|
||||
"DownloadClientStatusAllClientHealthCheckMessage": "Aucun client de téléchargement n'est disponible en raison d'échecs",
|
||||
"DownloadClients": "Clients de téléchargement",
|
||||
"DownloadClients": "Clients de télécharg.",
|
||||
"Dates": "Dates",
|
||||
"Date": "Date",
|
||||
"Delete": "Supprimer",
|
||||
@@ -404,9 +404,9 @@
|
||||
"Website": "Site internet",
|
||||
"AudioSearch": "Recherche de musique",
|
||||
"BookSearch": "Recherche de livres",
|
||||
"OnApplicationUpdate": "Sur la mise à jour de l'application",
|
||||
"OnApplicationUpdateHelpText": "Lors de la mise à jour de l'app",
|
||||
"IndexerNoDefinitionCheckHealthCheckMessage": "Les indexeurs ne sont pas définis et ne fonctionneront pas: {indexerNames}. Merci de les retirer et (ou) les ajouter à nouveau à {appName}",
|
||||
"OnApplicationUpdate": "Lors de la mise à jour de l'application",
|
||||
"OnApplicationUpdateHelpText": "Lors de la mise à jour de l'application",
|
||||
"IndexerNoDefinitionCheckHealthCheckMessage": "Les indexeurs ne sont pas définis et ne fonctionneront pas : {indexerNames}. Merci de les retirer et (ou) les ajouter à nouveau à {appName}.",
|
||||
"MovieSearch": "Recherche de films",
|
||||
"TvSearch": "Recherche de séries TV",
|
||||
"Application": "Applications",
|
||||
@@ -760,5 +760,12 @@
|
||||
"Default": "Par défaut",
|
||||
"GrabRelease": "Saisir Release",
|
||||
"ManualGrab": "Saisie manuelle",
|
||||
"Open": "Ouvrir"
|
||||
"Open": "Ouvrir",
|
||||
"Any": "Tous",
|
||||
"BuiltIn": "Intégré",
|
||||
"Script": "Script",
|
||||
"InfoUrl": "URL d'informations",
|
||||
"Redirected": "Rediriger",
|
||||
"PublishedDate": "Date de publication",
|
||||
"AllSearchResultsHiddenByFilter": "Tous les résultats sont masqués par le filtre appliqué"
|
||||
}
|
||||
|
||||
@@ -411,5 +411,12 @@
|
||||
"IndexerHDBitsSettingsMediums": "בינוני",
|
||||
"ProxyValidationBadRequest": "נכשל בדיקת ה- proxy. קוד קוד: {statusCode}",
|
||||
"CustomFilter": "מסננים מותאמים אישית",
|
||||
"GrabRelease": "שחרור תפוס"
|
||||
"GrabRelease": "שחרור תפוס",
|
||||
"BuiltIn": "נִבנָה בְּ",
|
||||
"Script": "תַסרִיט",
|
||||
"PublishedDate": "תאריך פרסום",
|
||||
"AddCategory": "הוסף קטגוריה",
|
||||
"ActiveApps": "אפליקציות פעילות",
|
||||
"ActiveIndexers": "אינדקסרים פעילים",
|
||||
"AllSearchResultsHiddenByFilter": "כל התוצאות מוסתרות על ידי המסנן שהוחל"
|
||||
}
|
||||
|
||||
@@ -356,5 +356,9 @@
|
||||
"IndexerHDBitsSettingsMediums": "मध्यम",
|
||||
"CustomFilter": "कस्टम फ़िल्टर",
|
||||
"ProxyValidationBadRequest": "प्रॉक्सी का परीक्षण करने में विफल। स्थिति कोड: {statusCode}",
|
||||
"GrabRelease": "पकड़ो रिलीज"
|
||||
"GrabRelease": "पकड़ो रिलीज",
|
||||
"BuiltIn": "में निर्मित",
|
||||
"Script": "लिपि",
|
||||
"PublishedDate": "प्रकाशित तिथि",
|
||||
"AllSearchResultsHiddenByFilter": "सभी परिणाम लागू फ़िल्टर द्वारा छिपे हुए हैं"
|
||||
}
|
||||
|
||||
@@ -178,5 +178,8 @@
|
||||
"Id": "ID",
|
||||
"CountApplicationsSelected": "{count} Kolekcija odabrano",
|
||||
"IndexerHDBitsSettingsCodecs": "Kodek",
|
||||
"Directory": "Direktorij"
|
||||
"Directory": "Direktorij",
|
||||
"BuiltIn": "Ugrađeno",
|
||||
"Redirected": "Preusmjeri",
|
||||
"AllSearchResultsHiddenByFilter": "Svi rezultati su skriveni zbog primjenjenog filtera"
|
||||
}
|
||||
|
||||
@@ -575,5 +575,11 @@
|
||||
"ManualGrab": "Megfog",
|
||||
"PrioritySettings": "Prioritás: {priority}",
|
||||
"ProxyValidationBadRequest": "Proxy tesztelése sikertelen. Állapotkód: {statusCode}",
|
||||
"Default": "Alapértelmezett"
|
||||
"Default": "Alapértelmezett",
|
||||
"BuiltIn": "Beépített",
|
||||
"Script": "Szkript",
|
||||
"Any": "Bármi",
|
||||
"PublishedDate": "Közzététel dátuma",
|
||||
"Redirected": "Átirányítás",
|
||||
"AllSearchResultsHiddenByFilter": "Az alkalmazott szűrők miatt, az összes keresési eredmény rejtve marad"
|
||||
}
|
||||
|
||||
@@ -84,5 +84,6 @@
|
||||
"Link": "Tautan",
|
||||
"Id": "ID",
|
||||
"IndexerHDBitsSettingsCodecs": "Codec",
|
||||
"ProxyValidationBadRequest": "Gagal menguji proxy. Kode Status: {statusCode}"
|
||||
"ProxyValidationBadRequest": "Gagal menguji proxy. Kode Status: {statusCode}",
|
||||
"AllSearchResultsHiddenByFilter": "Seluruh hasil disembunyikan karena penyaringan yang diterapkan"
|
||||
}
|
||||
|
||||
@@ -357,5 +357,9 @@
|
||||
"IndexerHDBitsSettingsMediums": "Miðlungs",
|
||||
"CustomFilter": "Sérsniðin síur",
|
||||
"ProxyValidationBadRequest": "Mistókst að prófa umboðsmann. Stöðukóði: {statusCode}",
|
||||
"GrabRelease": "Grípa losun"
|
||||
"GrabRelease": "Grípa losun",
|
||||
"BuiltIn": "Innbyggð",
|
||||
"Script": "Handrit",
|
||||
"PublishedDate": "Útgáfudagur",
|
||||
"AllSearchResultsHiddenByFilter": "Allar niðurstöður eru faldar af beittu síunni"
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"TagsSettingsSummary": "Vedi tutte le etichette e come vengono utilizzate. Le etichette non utilizzate possono essere rimosse",
|
||||
"SetTags": "Imposta Etichette",
|
||||
"SelectAll": "Seleziona Tutto",
|
||||
"Scheduled": "Programmato",
|
||||
"Scheduled": "Pianificato",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "La versione {0} non è una versione valida per le release di {appName}, non riceverai aggiornamenti",
|
||||
"ProxyResolveIpHealthCheckMessage": "Impossibile risolvere l'indirizzo IP per l'Host Configurato del Proxy {proxyHostName}",
|
||||
"NoChanges": "Nessuna Modifica",
|
||||
"NoChange": "Nessuna Modifica",
|
||||
"NoChanges": "Nessun Cambiamento",
|
||||
"NoChange": "Nessun Cambio",
|
||||
"LastWriteTime": "Orario di Ultima Scrittura",
|
||||
"Indexer": "Indicizzatore",
|
||||
"HideAdvanced": "Nascondi Avanzate",
|
||||
@@ -35,8 +35,8 @@
|
||||
"ShowAdvanced": "Mostra Avanzate",
|
||||
"Settings": "Impostazioni",
|
||||
"Security": "Sicurezza",
|
||||
"Search": "Cerca",
|
||||
"SaveChanges": "Salva Modifiche",
|
||||
"Search": "Ricerca",
|
||||
"SaveChanges": "Salva Cambiamenti",
|
||||
"RestoreBackup": "Ripristina Backup",
|
||||
"ReleaseStatus": "Stato Release",
|
||||
"Refresh": "Aggiorna",
|
||||
@@ -46,7 +46,7 @@
|
||||
"Proxy": "Proxy",
|
||||
"Protocol": "Protocollo",
|
||||
"Options": "Opzioni",
|
||||
"MoreInfo": "Maggiori Info",
|
||||
"MoreInfo": "Ulteriori Informazioni",
|
||||
"Logging": "Logging",
|
||||
"LogFiles": "File di Log",
|
||||
"Language": "Lingua",
|
||||
@@ -91,15 +91,15 @@
|
||||
"CertificateValidation": "Convalida del Certificato",
|
||||
"Cancel": "Annulla",
|
||||
"BypassProxyForLocalAddresses": "Evita il Proxy per gli Indirizzi Locali",
|
||||
"Branch": "Ramo",
|
||||
"Branch": "Branca",
|
||||
"BindAddressHelpText": "Indirizzi IP validi, localhost o '*' per tutte le interfacce",
|
||||
"BindAddress": "Indirizzo di Ascolto",
|
||||
"Backups": "Backups",
|
||||
"Backups": "Backup",
|
||||
"BackupRetentionHelpText": "I backup più vecchi del periodo specificato saranno cancellati automaticamente",
|
||||
"BackupIntervalHelpText": "Intervallo fra i backup automatici",
|
||||
"BackupFolderHelpText": "I percorsi relativi saranno nella cartella AppData di {appName}",
|
||||
"Automatic": "Automatico",
|
||||
"AuthenticationMethodHelpText": "Inserisci Username e Password per accedere a {appName}",
|
||||
"AuthenticationMethodHelpText": "Utilizza nome utente e password per accedere a {appName}",
|
||||
"Authentication": "Autenticazione",
|
||||
"ApplyTags": "Applica Etichette",
|
||||
"Apply": "Applica",
|
||||
@@ -109,14 +109,14 @@
|
||||
"Warn": "Attenzione",
|
||||
"Type": "Tipo",
|
||||
"Title": "Titolo",
|
||||
"Time": "Ora",
|
||||
"TestAll": "Prova Tutti",
|
||||
"Test": "Test",
|
||||
"Time": "Orario",
|
||||
"TestAll": "Prova Tutto",
|
||||
"Test": "Prova",
|
||||
"TableOptionsColumnsMessage": "Scegli quali colonne rendere visibili ed il loro ordine",
|
||||
"TableOptions": "Opzioni della tabella",
|
||||
"TableOptions": "Opzioni Tabella",
|
||||
"SystemTimeCheckMessage": "L'orario di sistema è sbagliato di più di un giorno. Le attività pianificate potrebbero non essere eseguite correttamente fino alla correzione",
|
||||
"Source": "Fonte",
|
||||
"Shutdown": "Spegni",
|
||||
"Shutdown": "Spegnimento",
|
||||
"Seeders": "Seeders",
|
||||
"Save": "Salva",
|
||||
"Restart": "Riavvia",
|
||||
@@ -139,15 +139,15 @@
|
||||
"DeleteNotification": "Cancella Notifica",
|
||||
"DeleteDownloadClient": "Cancella Client di Download",
|
||||
"DeleteBackup": "Cancella Backup",
|
||||
"DatabaseMigration": "Migrazione DB",
|
||||
"DatabaseMigration": "Migrazione Database",
|
||||
"ConnectSettings": "Impostazioni Collegamento",
|
||||
"ConnectionLost": "Connessione Persa",
|
||||
"Component": "Componente",
|
||||
"Columns": "Colonne",
|
||||
"DeleteBackupMessageText": "Sei sicuro di voler cancellare il backup '{0}'?",
|
||||
"DeleteBackupMessageText": "Sei sicuro di voler cancellare il backup '{name}'?",
|
||||
"CancelPendingTask": "Sei sicuro di voler cancellare questa operazione in sospeso?",
|
||||
"BranchUpdateMechanism": "Ramo utilizzato dal sistema di aggiornamento esterno",
|
||||
"BranchUpdate": "Ramo da usare per aggiornare {appName}",
|
||||
"BranchUpdate": "Branca da usare per aggiornare {appName}",
|
||||
"AddingTag": "Aggiungendo etichetta",
|
||||
"Password": "Password",
|
||||
"OnHealthIssueHelpText": "Quando c'è un problema",
|
||||
@@ -162,41 +162,41 @@
|
||||
"SettingsEnableColorImpairedMode": "Abilità la Modalità Daltonica",
|
||||
"SendAnonymousUsageData": "Invia dati anonimi sull'uso",
|
||||
"ScriptPath": "Percorso dello script",
|
||||
"RssIsNotSupportedWithThisIndexer": "RSS non è supportato con questo Indicizzatore",
|
||||
"RssIsNotSupportedWithThisIndexer": "RSS non è supportato con questo indicizzatore",
|
||||
"Retention": "Ritenzione",
|
||||
"Result": "Risultato",
|
||||
"Restore": "Ripristina",
|
||||
"RestartRequiredHelpTextWarning": "Richiede il riavvio per avere effetto",
|
||||
"RestartProwlarr": "Riavvia {appName}",
|
||||
"RestartNow": "Riavvia adesso",
|
||||
"RestartNow": "Riavvia ora",
|
||||
"ResetAPIKey": "Resetta la Chiave API",
|
||||
"Reset": "Resetta",
|
||||
"Reset": "Reimposta",
|
||||
"RemovingTag": "Eliminando l'etichetta",
|
||||
"RemoveFilter": "Rimuovi filtro",
|
||||
"RemovedFromTaskQueue": "Rimosso dalla coda lavori",
|
||||
"RefreshMovie": "Aggiorna il Film",
|
||||
"ReadTheWikiForMoreInformation": "Leggi la Wiki per maggiori informazioni",
|
||||
"ReadTheWikiForMoreInformation": "Leggi la Wiki per più informazioni",
|
||||
"ProwlarrSupportsAnyIndexer": "{appName} supporta molti indicizzatori oltre a qualsiasi indicizzatore che utilizza lo standard Newznab/Torznab utilizzando \"Generic Newznab\" (per usenet) o \"Generic Torznab\" (per torrent). Cerca e seleziona il tuo indicizzatore da qua sotto.",
|
||||
"ProwlarrSupportsAnyDownloadClient": "{appName} supporta qualunque client di download elencato sotto.",
|
||||
"ProxyUsernameHelpText": "Devi inserire nome utente e password solo se richiesto. Altrimenti lascia vuoto.",
|
||||
"ProxyType": "Tipo di Proxy",
|
||||
"ProxyType": "Tipo Proxy",
|
||||
"ProxyPasswordHelpText": "Devi inserire nome utente e password solo se richiesto. Altrimenti lascia vuoto.",
|
||||
"ProxyBypassFilterHelpText": "Usa ',' come separatore, e '*.' come jolly per i sottodomini",
|
||||
"PortNumber": "Numero di porta",
|
||||
"ProxyBypassFilterHelpText": "Usa ',' come separatore, e '*.' come wildcard per i sottodomini",
|
||||
"PortNumber": "Numero Porta",
|
||||
"Port": "Porta",
|
||||
"PendingChangesStayReview": "Rimani e rivedi modifiche",
|
||||
"PendingChangesMessage": "Hai cambiamenti non salvati, sicuro di voler abbandonare la pagina?",
|
||||
"PendingChangesStayReview": "Rimani e rivedi i cambiamenti",
|
||||
"PendingChangesMessage": "Hai dei cambiamenti non salvati, sei sicuro di volere lasciare questa pagina?",
|
||||
"PendingChangesDiscardChanges": "Abbandona le modifiche ed esci",
|
||||
"PageSizeHelpText": "Numero di voci da mostrare in ogni pagina",
|
||||
"PackageVersion": "Versione del Pacchetto",
|
||||
"OpenBrowserOnStart": "Apri il browser all'avvio",
|
||||
"OpenBrowserOnStart": "Apri browser all'avvio",
|
||||
"NoUpdatesAreAvailable": "Nessun aggiornamento disponibile",
|
||||
"NoTagsHaveBeenAddedYet": "Nessuna etichetta è ancora stata aggiunta",
|
||||
"NoLogFiles": "Nessun file di log",
|
||||
"NoLeaveIt": "No, Lascialo",
|
||||
"NoBackupsAreAvailable": "Nessun Backup disponibile",
|
||||
"NoBackupsAreAvailable": "Nessun backup disponibile",
|
||||
"New": "Nuovo",
|
||||
"Mode": "Modo",
|
||||
"Mode": "Modalità",
|
||||
"Mechanism": "Meccanismo",
|
||||
"Manual": "Manuale",
|
||||
"MaintenanceRelease": "Release di Manutenzione: correzione di bug e altri miglioramenti. Vedi la storia dei Commit su Github per maggiori dettagli",
|
||||
@@ -209,8 +209,7 @@
|
||||
"IgnoredAddresses": "Indirizzi Ignorati",
|
||||
"GeneralSettings": "Impostazioni Generali",
|
||||
"ForMoreInformationOnTheIndividualDownloadClients": "Per più informazioni sui singoli client di download clicca sui pulsanti info.",
|
||||
"Fixed": "Fissato",
|
||||
"FilterPlaceHolder": "Cerca Indicizzatori",
|
||||
"FilterPlaceHolder": "Cerca indicizzatori",
|
||||
"ExistingTag": "Etichetta esistente",
|
||||
"Exception": "Eccezione",
|
||||
"ErrorLoadingContents": "Errore nel caricare i contenuti",
|
||||
@@ -219,22 +218,22 @@
|
||||
"EnableInteractiveSearch": "Abilita la Ricerca Interattiva",
|
||||
"EnableAutomaticSearchHelpText": "Sarà usata quando la ricerca automatica è eseguita dalla l'intrfaccia o da {appName}",
|
||||
"EnableAutomaticSearch": "Attiva la Ricerca Automatica",
|
||||
"DeleteTagMessageText": "Sei sicuro di voler eliminare l'etichetta '{0}'?",
|
||||
"DeleteNotificationMessageText": "Sei sicuro di voler eliminare la notifica '{0}'?",
|
||||
"DeleteDownloadClientMessageText": "Sei sicuro di voler eliminare il client di download '{0}'?",
|
||||
"DeleteTagMessageText": "Sei sicuro di voler eliminare l'etichetta '{label}'?",
|
||||
"DeleteNotificationMessageText": "Sei sicuro di voler eliminare la notifica '{name}'?",
|
||||
"DeleteDownloadClientMessageText": "Sei sicuro di voler eliminare il client di download '{name}'?",
|
||||
"BeforeUpdate": "Prima dell'aggiornamento",
|
||||
"Usenet": "Usenet",
|
||||
"Uptime": "Tempo di attività",
|
||||
"YesCancel": "Si, Cancella",
|
||||
"YesCancel": "Sì, Cancella",
|
||||
"Version": "Versione",
|
||||
"Username": "Nome utente",
|
||||
"Username": "Nome Utente",
|
||||
"UseProxy": "Usa Proxy",
|
||||
"UrlBaseHelpText": "Per il supporto al reverse proxy, di default è vuoto",
|
||||
"URLBase": "Base Url",
|
||||
"UpdateScriptPathHelpText": "Percorso verso uno script personalizzato che prende un pacchetto di aggiornamento estratto e gestisce il resto del processo di aggiornamento",
|
||||
"UpdateMechanismHelpText": "Usa il sistema di aggiornamento interno di {appName} o uno script",
|
||||
"UpdateMechanismHelpText": "Usa il sistema di aggiornamento incorporato di {appName} o uno script",
|
||||
"UpdateAutomaticallyHelpText": "Scarica e installa automaticamente gli aggiornamenti. Sarai comunque in grado in installarli da Sistema: Aggiornamenti",
|
||||
"UnsavedChanges": "Modifiche non salvate",
|
||||
"UnsavedChanges": "Cambiamenti Non Salvati",
|
||||
"UnableToLoadUISettings": "Impossibile caricare le impostazioni interfaccia",
|
||||
"UnableToLoadTags": "Impossibile caricare le Etichette",
|
||||
"UnableToLoadNotifications": "Impossibile caricare le Notifiche",
|
||||
@@ -255,7 +254,7 @@
|
||||
"TagIsNotUsedAndCanBeDeleted": "L'etichetta non è in uso e può essere eliminata",
|
||||
"TagCannotBeDeletedWhileInUse": "Non può essere cancellato mentre è in uso",
|
||||
"SuggestTranslationChange": "Suggerisci un cambio nella traduzione",
|
||||
"StartupDirectory": "Cartella di avvio",
|
||||
"StartupDirectory": "Cartella di Avvio",
|
||||
"StartTypingOrSelectAPathBelow": "Comincia a digitare o seleziona un percorso sotto",
|
||||
"SSLPort": "Porta SSL",
|
||||
"SSLCertPathHelpText": "Percorso file pfx",
|
||||
@@ -300,15 +299,15 @@
|
||||
"Donations": "Donazioni",
|
||||
"EnableRssHelpText": "Abilita feed RSS per l'Indicizzatore",
|
||||
"HomePage": "Pagina Iniziale",
|
||||
"Id": "Id",
|
||||
"Id": "ID",
|
||||
"IndexerHealthCheckNoIndexers": "Nessun Indicizzatore abilitato, {appName} non restituirà risultati di ricerca",
|
||||
"EnableRss": "Abilita RSS",
|
||||
"NoLinks": "Nessun Collegamento",
|
||||
"Rss": "RSS",
|
||||
"Wiki": "Wiki",
|
||||
"AllIndexersHiddenDueToFilter": "Tutti gli Indexer sono nascosti a causa del filtro applicato.",
|
||||
"DeleteApplicationMessageText": "Sei sicuro di voler eliminare l'applicazione '{0}'?",
|
||||
"DeleteIndexerProxyMessageText": "Sei sicuro di voler eliminare il proxy '{0}'?",
|
||||
"DeleteApplicationMessageText": "Sei sicuro di voler eliminare l'applicazione '{name}'?",
|
||||
"DeleteIndexerProxyMessageText": "Sei sicuro di voler eliminare il proxy '{name}'?",
|
||||
"Presets": "Preset",
|
||||
"SearchIndexers": "Cerca Indicizzatori",
|
||||
"UnableToAddANewIndexerProxyPleaseTryAgain": "Impossibile aggiungere un nuovo proxy per l'Indicizzatore, riprova.",
|
||||
@@ -342,7 +341,7 @@
|
||||
"MappedDrivesRunningAsService": "Le unità di rete mappate non sono disponibili eseguendo come servizio di Windows. Vedere le FAQ per maggiori informazioni",
|
||||
"No": "No",
|
||||
"UnableToLoadIndexers": "Impossibile caricare gli Indicizzatori",
|
||||
"Yes": "Si",
|
||||
"Yes": "Sì",
|
||||
"AddIndexerProxy": "Aggiungi proxy dell'Indexer",
|
||||
"AudioSearch": "Ricerca Audio",
|
||||
"BookSearch": "Ricerca Libri",
|
||||
@@ -385,7 +384,7 @@
|
||||
"IndexerDetails": "Dettagli dell'Indicizzatore",
|
||||
"IndexerInfo": "Info sull'Indicizzatore",
|
||||
"IndexerName": "Nome dell'Indicizzatore",
|
||||
"IndexerNoDefinitionCheckHealthCheckMessage": "Gli indicizzatori non hanno una definizione e non funzioneranno: {0}. Si prega di rimuoverli e/o di riaggiungerli a {appName}",
|
||||
"IndexerNoDefinitionCheckHealthCheckMessage": "Gli indicizzatori non hanno una definizione e non funzioneranno: {indexerNames}. Si prega di rimuoverli e/o di riaggiungerli a {appName}",
|
||||
"HistoryCleanup": "Pulizia della Cronologia",
|
||||
"IndexerRss": "RSS dell'Indicizzatore",
|
||||
"IndexerSite": "Sito dell'Indicizzatore",
|
||||
@@ -434,10 +433,10 @@
|
||||
"MinimumSeeders": "Seeder Minimi",
|
||||
"InstanceName": "Nome Istanza",
|
||||
"InstanceNameHelpText": "Nome istanza nella scheda e per il nome dell'app nel Syslog",
|
||||
"ThemeHelpText": "Cambia il Tema dell'interfaccia dell’applicazione, il Tema 'Auto' userà il suo Tema di Sistema per impostare la modalità Chiara o Scura. Ispirato da {0}",
|
||||
"ThemeHelpText": "Cambia il Tema dell'interfaccia dell’applicazione, il Tema 'Auto' userà il tuo Tema di Sistema per impostare la modalità Chiara o Scura. Ispirato da {inspiredBy}.",
|
||||
"LastDuration": "Ultima Durata",
|
||||
"LastExecution": "Ultima esecuzione",
|
||||
"Queued": "In coda",
|
||||
"Queued": "In Coda",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "Tutte le app non disponibili da almeno 6 ore a causa di errori",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "Alcune app non sono disponibili da almeno 6 ore a causa di errori: {0}",
|
||||
"Duration": "Durata",
|
||||
@@ -466,9 +465,9 @@
|
||||
"ApplyTagsHelpTextAdd": "Aggiungi: Aggiunge le etichette alla lista esistente di etichette",
|
||||
"ApplyTagsHelpTextHowToApplyApplications": "Come applicare etichette agli autori selezionati",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Come applicare etichette agli indicizzatori selezionati",
|
||||
"CountIndexersSelected": "{0} indicizzatore(i) selezionato(i)",
|
||||
"DeleteSelectedApplicationsMessageText": "Sei sicuro di voler eliminare l'indexer '{0}'?",
|
||||
"DeleteSelectedDownloadClientsMessageText": "Sei sicuro di voler eliminare l'indexer '{0}'?",
|
||||
"CountIndexersSelected": "{count} indicizzatore(i) selezionato(i)",
|
||||
"DeleteSelectedApplicationsMessageText": "Sei sicuro di voler eliminare {count} applicazione(i) selezionata(e)?",
|
||||
"DeleteSelectedDownloadClientsMessageText": "Sei sicuro di voler eliminare i '{count}' client di download selezionato/i?",
|
||||
"SelectIndexers": "Cerca Indicizzatori",
|
||||
"Track": "Traccia",
|
||||
"Book": "Libro",
|
||||
@@ -477,22 +476,21 @@
|
||||
"ApplyTagsHelpTextReplace": "Sostituire: Sostituisce le etichette con quelle inserite (non inserire nessuna etichette per eliminarle tutte)",
|
||||
"DownloadClientPriorityHelpText": "Dai priorità a multipli Client di download. Round-Robin è usato per i client con la stessa priorità.",
|
||||
"DeleteSelectedDownloadClients": "Cancella i Client di Download",
|
||||
"DeleteSelectedIndexersMessageText": "Sei sicuro di voler eliminare l'indexer '{0}'?",
|
||||
"Album": "Album",
|
||||
"Artist": "Artista",
|
||||
"Label": "Etichetta",
|
||||
"More": "Di più",
|
||||
"More": "Altro",
|
||||
"Season": "Stagione",
|
||||
"Year": "Anno",
|
||||
"UpdateAvailableHealthCheckMessage": "É disponibile un nuovo aggiornamento",
|
||||
"UpdateAvailableHealthCheckMessage": "Nuovo aggiornamento disponibile",
|
||||
"Author": "Autore",
|
||||
"ApplyChanges": "Applica Cambiamenti",
|
||||
"ApiKeyValidationHealthCheckMessage": "Aggiorna la tua chiave API in modo che abbia una lunghezza di almeno {length} caratteri. Puoi farlo dalle impostazioni o dal file di configurazione",
|
||||
"DeleteAppProfileMessageText": "Sicuro di voler cancellare il profilo di qualità {0}",
|
||||
"RecentChanges": "Cambiamenti recenti",
|
||||
"DeleteAppProfileMessageText": "Sicuro di voler cancellare il profilo dell'app '{name}'?",
|
||||
"RecentChanges": "Cambiamenti Recenti",
|
||||
"WhatsNew": "Cosa c'è di nuovo?",
|
||||
"ConnectionLostReconnect": "Radarr cercherà di connettersi automaticamente, oppure clicca su ricarica qui sotto.",
|
||||
"ConnectionLostToBackend": "Radarr ha perso la connessione al backend e dovrà essere ricaricato per ripristinare la funzionalità.",
|
||||
"ConnectionLostReconnect": "{appName} cercherà di connettersi automaticamente, oppure clicca su ricarica qui sotto.",
|
||||
"ConnectionLostToBackend": "{appName} ha perso la connessione al backend e dovrà essere ricaricato per ripristinare la funzionalità.",
|
||||
"minutes": "Minuti",
|
||||
"AddConnection": "Aggiungi Connessione",
|
||||
"NotificationStatusAllClientHealthCheckMessage": "Tutte le applicazioni non sono disponibili a causa di errori",
|
||||
@@ -520,9 +518,9 @@
|
||||
"AddIndexerProxyImplementation": "Aggiungi indicizzatore - {implementationName}",
|
||||
"EditApplicationImplementation": "Aggiungi Condizione - {implementationName}",
|
||||
"CountApplicationsSelected": "{count} Collezione(i) Selezionate",
|
||||
"EditConnectionImplementation": "Aggiungi Connessione - {implementationName}",
|
||||
"EditDownloadClientImplementation": "Aggiungi un Client di Download - {implementationName}",
|
||||
"EditIndexerImplementation": "Aggiungi indicizzatore - {implementationName}",
|
||||
"EditConnectionImplementation": "Modifica Connessione - {implementationName}",
|
||||
"EditDownloadClientImplementation": "Modifica Client di Download - {implementationName}",
|
||||
"EditIndexerImplementation": "Modifica Indicizzatore - {implementationName}",
|
||||
"EditIndexerProxyImplementation": "Aggiungi indicizzatore - {implementationName}",
|
||||
"AdvancedSettingsShownClickToHide": "Impostazioni avanzate mostrate, clicca per nasconderle",
|
||||
"AdvancedSettingsHiddenClickToShow": "Impostazioni avanzate nascoste, clicca per mostrarle",
|
||||
@@ -532,9 +530,121 @@
|
||||
"ActiveIndexers": "Indicizzatori Attivi",
|
||||
"IndexerBeyondHDSettingsSearchTypes": "Tipi di Ricerca",
|
||||
"Directory": "Cartella",
|
||||
"CustomFilter": "Filtri Personalizzati",
|
||||
"CustomFilter": "Filtro Personalizzato",
|
||||
"IndexerHDBitsSettingsCodecs": "Codec",
|
||||
"IndexerHDBitsSettingsMediums": "medio",
|
||||
"GrabRelease": "Preleva Release",
|
||||
"ProxyValidationBadRequest": "Il test del proxy è fallito. Codice Stato: {statusCode}"
|
||||
"ProxyValidationBadRequest": "Il test del proxy è fallito. Codice Stato: {statusCode}",
|
||||
"Discord": "Discord",
|
||||
"Donate": "Dona",
|
||||
"Destination": "Destinazione",
|
||||
"DownloadClientFreeboxSettingsApiUrl": "API URL",
|
||||
"DownloadClientFreeboxSettingsAppId": "ID App",
|
||||
"DownloadClientFreeboxSettingsAppToken": "Token App",
|
||||
"DownloadClientPneumaticSettingsNzbFolder": "Cartella Nzb",
|
||||
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Questa cartella dovrà essere raggiungibile da XBMC",
|
||||
"DownloadClientRTorrentSettingsUrlPath": "Percorso Url",
|
||||
"Default": "Predefinito",
|
||||
"DownloadClientPneumaticSettingsStrmFolder": "Cartella Strm",
|
||||
"IndexerDisabled": "Indexer Disattivato",
|
||||
"GoToApplication": "Vai all'applicazione",
|
||||
"AreYouSureYouWantToDeleteIndexer": "Sei sicuro di voler eliminare '{name}' da {appName}?",
|
||||
"IndexerStatus": "Stato Indicizzatore",
|
||||
"XmlRpcPath": "Percorso XML RPC",
|
||||
"EditCategory": "Modifica Categoria",
|
||||
"IndexerSettingsAdditionalParameters": "Parametri Addizionali",
|
||||
"IndexerSettingsApiPath": "Percorso API",
|
||||
"IndexerSettingsVipExpiration": "Scadenza VIP",
|
||||
"DefaultCategory": "Categoria Predefinita",
|
||||
"DownloadClientFloodSettingsAdditionalTags": "Tag addizionali",
|
||||
"IndexerHDBitsSettingsMediumsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.",
|
||||
"IndexerHDBitsSettingsOrigins": "Origini",
|
||||
"IndexerHDBitsSettingsOriginsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.",
|
||||
"IndexerSettingsCookie": "Cookie",
|
||||
"DeleteSelectedApplications": "Elimina Applicazioni Selezionate",
|
||||
"IndexerHDBitsSettingsCodecsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.",
|
||||
"IndexerSettingsApiUser": "Utente API",
|
||||
"PrioritySettings": "Priorità: {priority}",
|
||||
"CountDownloadClientsSelected": "{count} client di download selezionato/i",
|
||||
"NotificationsTelegramSettingsIncludeAppName": "Includi {appName} nel Titolo",
|
||||
"Menu": "Menu",
|
||||
"NoIndexersFound": "Nessun indicizzatore trovato",
|
||||
"PasswordConfirmation": "Conferma Password",
|
||||
"NoHistoryFound": "Nessun storico trovato",
|
||||
"DeleteSelectedIndexersMessageText": "Sei sicuro di voler eliminare {count} applicazione(i) selezionata(e)?",
|
||||
"UsenetBlackholeNzbFolder": "Cartella Nzb",
|
||||
"VipExpiration": "Scadenza VIP",
|
||||
"OverrideAndAddToDownloadClient": "Sovrascrivi e aggiungi alla coda di download",
|
||||
"BasicSearch": "Ricerca basica",
|
||||
"CountIndexersAvailable": "{count} indicizzatore/i disponibili",
|
||||
"EditSelectedIndexers": "Modifica Indicizzatori Selezionati",
|
||||
"FoundCountReleases": "Trovate {itemCount} release",
|
||||
"ManageApplications": "Gestisci Applicazioni",
|
||||
"ManageDownloadClients": "Gestisci Clients di Download",
|
||||
"HistoryDetails": "Dettagli Storico",
|
||||
"NotificationsEmailSettingsUseEncryption": "Usa Crittografia",
|
||||
"SearchAllIndexers": "Cerca tutti gli indicizzatori",
|
||||
"SearchCountIndexers": "Cerca {count} indicizzatore/i",
|
||||
"SearchQueries": "Cerca Richieste",
|
||||
"SeedRatio": "Rapporto Seed",
|
||||
"TorznabUrl": "Url Torznab",
|
||||
"TorrentBlackholeTorrentFolder": "Cartella Torrent",
|
||||
"UseSsl": "Usa SSL",
|
||||
"days": "giorni",
|
||||
"IndexerCategories": "Categorie degli Indicizzatori",
|
||||
"IndexerTorrentSyndikatSettingsApiKeyHelpText": "API Key Sito",
|
||||
"LabelIsRequired": "Etichetta richiesta",
|
||||
"NoIndexerHistory": "Nessun storico trovato per questo indicizzatore",
|
||||
"RssFeed": "Feed RSS",
|
||||
"AverageResponseTimesMs": "Tempo di Risposta Medio dell'Indicizzatore (ms)",
|
||||
"DeleteSelectedIndexer": "Elimina Indicizzatore Selezionato",
|
||||
"DisabledUntil": "Disattiva fino",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "Aggiungi un prefisso all'url del json di deluge, vedi {url}",
|
||||
"Implementation": "Implementazione",
|
||||
"ManageClients": "Gestisci Clients",
|
||||
"NewznabUrl": "Url Newznab",
|
||||
"NoApplicationsFound": "Nessuna applicazione trovata",
|
||||
"IndexerSettingsBaseUrl": "Url Base",
|
||||
"IndexerId": "ID Indicizzatore",
|
||||
"NoDownloadClientsFound": "Nessun client di download trovato",
|
||||
"BlackholeFolderHelpText": "Cartella nella quale {appName} salverà i file di tipo {extension}",
|
||||
"DownloadClientNzbgetSettingsAddPausedHelpText": "Questa opzione richiede almeno la versione 16.0 di NzbGet",
|
||||
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Scarica in ordine sequenziale (qBittorrent 4.1.0+)",
|
||||
"DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una connessione sicura. Vedi Opzioni -> Web UI -> 'Usa HTTPS invece di HTTP' in qBittorrent.",
|
||||
"DownloadClientRTorrentSettingsAddStopped": "Aggiungi Fermato",
|
||||
"DownloadClientSettingsInitialState": "Stato Iniziale",
|
||||
"DownloadClientSettingsInitialStateHelpText": "Stato iniziale per i torrent aggiunti a {clientName}",
|
||||
"DownloadClientSettingsAddPaused": "Aggiungi In Pausa",
|
||||
"DownloadClientSettingsUseSslHelpText": "Usa connessione sicura quando connetti a {clientName}",
|
||||
"IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent",
|
||||
"IndexerSettingsApiPathHelpText": "Percorso API, solitamente {url}",
|
||||
"IndexerSettingsBaseUrlHelpText": "Seleziona quale url base {appName} userà per le richieste al sito",
|
||||
"NoIndexerCategories": "Nessuna categoria trovata per questo indicizzatore",
|
||||
"SecretToken": "Secret Token",
|
||||
"SeedRatioHelpText": "Il rapporto che un torrent dovrebbe raggiungere prima di essere fermato, vuoto è il predefinito dell'app",
|
||||
"TotalQueries": "Totale Richieste",
|
||||
"IndexerHistoryLoadError": "Errore caricando lo storico dell'indicizzatore",
|
||||
"DeleteSelectedIndexers": "Elimina Indicizzatori Selezionati",
|
||||
"InvalidUILanguage": "L'interfaccia è impostata in una lingua non valida, correggi e salva le tue impostazioni",
|
||||
"IndexerSettingsSeedRatio": "Rapporto Seed",
|
||||
"IndexerSettingsRssKey": "Chiave RSS",
|
||||
"RssQueries": "Richieste RSS",
|
||||
"DownloadClientQbittorrentSettingsSequentialOrder": "Ordine Sequenziale",
|
||||
"External": "Esterno",
|
||||
"IndexerNewznabSettingsAdditionalParametersHelpText": "Parametri Newznab addizionali",
|
||||
"SelectDownloadClientModalTitle": "{modalTitle} - Seleziona Client di Download",
|
||||
"DownloadClientSettingsDestinationHelpText": "Specifica manualmente la destinazione dei download, lascia vuoti per usare la predefinita",
|
||||
"IndexerDownloadClientHealthCheckMessage": "Indicizzatori con client di download non validi: {indexerNames}.",
|
||||
"SeedTimeHelpText": "Il rapporto che un torrent dovrebbe raggiungere prima di essere fermato, vuoto è il predefinito dell'app",
|
||||
"IndexerPassThePopcornSettingsApiKeyHelpText": "API Key Sito",
|
||||
"IndexerNzbIndexSettingsApiKeyHelpText": "API Key Sito",
|
||||
"IndexerNewznabSettingsApiKeyHelpText": "API Key Sito",
|
||||
"Fixed": "Fissato",
|
||||
"Any": "Qualunque",
|
||||
"BuiltIn": "Incluso",
|
||||
"Script": "Script",
|
||||
"InfoUrl": "URL Info",
|
||||
"PublishedDate": "Data Pubblicazione",
|
||||
"Redirected": "Reindirizzamento",
|
||||
"AllSearchResultsHiddenByFilter": "Tutti i risultati sono nascosti dal filtro"
|
||||
}
|
||||
|
||||
@@ -357,5 +357,9 @@
|
||||
"IndexerHDBitsSettingsMediums": "中",
|
||||
"CustomFilter": "カスタムフィルター",
|
||||
"ProxyValidationBadRequest": "プロキシのテストに失敗しました。 StatusCode:{statusCode}",
|
||||
"GrabRelease": "グラブリリース"
|
||||
"GrabRelease": "グラブリリース",
|
||||
"Script": "脚本",
|
||||
"BuiltIn": "ビルトイン",
|
||||
"PublishedDate": "公開日",
|
||||
"AllSearchResultsHiddenByFilter": "すべての結果は、適用されたフィルターによって非表示になります"
|
||||
}
|
||||
|
||||
@@ -357,5 +357,8 @@
|
||||
"IndexerHDBitsSettingsMediums": "매질",
|
||||
"CustomFilter": "사용자 지정 필터",
|
||||
"GrabRelease": "그랩 릴리스",
|
||||
"ProxyValidationBadRequest": "프록시를 테스트하지 못했습니다. StatusCode : {statusCode}"
|
||||
"ProxyValidationBadRequest": "프록시를 테스트하지 못했습니다. StatusCode : {statusCode}",
|
||||
"BuiltIn": "내장",
|
||||
"PublishedDate": "발행일",
|
||||
"AllSearchResultsHiddenByFilter": "적용된 필터에 의해 모든 결과가 숨겨집니다."
|
||||
}
|
||||
|
||||
@@ -152,5 +152,9 @@
|
||||
"AddConnectionImplementation": "Legg til betingelse - {implementationName}",
|
||||
"AddIndexerImplementation": "Legg til betingelse - {implementationName}",
|
||||
"AddIndexerProxyImplementation": "Legg til betingelse - {implementationName}",
|
||||
"UnableToAddANewApplicationPleaseTryAgain": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen"
|
||||
"UnableToAddANewApplicationPleaseTryAgain": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen",
|
||||
"EditIndexerProxyImplementation": "Legg til betingelse - {implementationName}",
|
||||
"UnableToAddANewAppProfilePleaseTryAgain": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen",
|
||||
"BuiltIn": "Bygget inn",
|
||||
"AllSearchResultsHiddenByFilter": "Alle resultatene er skjult av det anvendte filteret"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user