Compare commits

..

39 Commits

Author SHA1 Message Date
Weblate
2f67d2813a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: seidnerj <jonathan.seidner@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-07-27 00:19:19 +03:00
Bogdan
9a7a5fdc38 Remove PropTypes 2024-07-26 06:40:34 +03:00
Bogdan
f1fdec6822 Convert Add Indexer Modal to Typescript 2024-07-26 06:25:37 +03:00
Bogdan
5464b23329 Sort indexer queries stats by a sum of all 3 types 2024-07-26 01:09:04 +03:00
Bogdan
4c99971882 Improve messaging on no results with applied filter 2024-07-25 08:08:48 +03:00
Weblate
cc7769b601 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-07-24 19:42:16 +03:00
Bogdan
cb2ed7daf9 Fixed: Improve elapsed time collecting for grabs 2024-07-22 19:52:12 +03:00
Bogdan
78508094c8 Bump some frontend libraries 2024-07-22 01:02:25 +03:00
Bogdan
b0f755a30c Fixed: (Nebulance) Searching for daily episodes using ids 2024-07-22 00:38:13 +03:00
Bogdan
9d1384792a Bump version to 1.21.2 2024-07-21 18:06:41 +03:00
Bogdan
ea17116998 Fixed: (Nebulance) Avoid requests for release calls that are 2 characters or fewer 2024-07-21 02:39:02 +03:00
Servarr
2c23681fc5 Automated API Docs update 2024-07-21 01:53:11 +03:00
Bogdan
17aa2832ea New: Split average response time statistics for queries and grabs 2024-07-21 01:01:03 +03:00
Bogdan
5f3a329ef2 Don't show null for non-cached indexer queries 2024-07-20 20:36:01 +03:00
Bogdan
96f49da79e New: Improve history details for release grabs 2024-07-20 19:50:20 +03:00
Bogdan
c7dfde0ce9 Improve messaging for invalid request for M-Team-TP 2024-07-19 19:49:17 +03:00
Bogdan
8cf32020f7 New: Bump dotnet to 6.0.32 2024-07-19 19:39:06 +03:00
Weblate
a5ed5a0e60 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Rauniik <raunerjakub@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-07-19 00:05:13 +03:00
Bogdan
3279936fc9 New: Litestream compatibility for SQLite (#2179) 2024-07-18 23:33:55 +03:00
Bogdan
8abccc709e Use natural sorting for remaining lists of items in the UI 2024-07-18 22:49:43 +03:00
Mark McDowall
76f30e7682 New: Use natural sorting for lists of items in the UI
(cherry picked from commit 1a1c8e6c08a6db5fcd2b5d17e65fa1f943d2e746)
2024-07-18 21:35:06 +03:00
Mark McDowall
ab289b3e42 New: Show update settings on all platforms
(cherry picked from commit c023fc700896c7f0751c4ac63c4e1a89d6e1a9bb)
2024-07-18 21:22:01 +03:00
Bogdan
ef7e04065c Fixed: (BeyondHD) Don't die on invalid TMDb ids 2024-07-17 02:36:58 +03:00
Bogdan
d1084039b3 Bump version to 1.21.1 2024-07-14 12:29:09 +03:00
Bogdan
7bada440d2 Log invalid torrent files contents as debug
Fixes #2169
2024-07-12 13:00:23 +03:00
Bogdan
803c4752db Fixed: Sending health restored notifications with Gotify
Fixed #2176
2024-07-11 13:35:02 +03:00
Bogdan
c0777474c0 Bump Polly 2024-07-09 23:47:23 +03:00
Weblate
66dcea5604 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Serhii Matrunchyk <serhii@digitalidea.studio>
Co-authored-by: Taylan Tatlı <taylantatli90@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: quek76 <quek@libertysurf.fr>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-07-09 11:06:17 +03:00
Qstick
a2a12d2450 Update SonarCloud pipeline versions (#2171)
* Update SonarCloud pipeline versions

* Update reportgenerator to remove PublishCodeCoverage dep warnings
2024-07-07 17:55:24 +03:00
Bogdan
39593bd5a8 Bump version to 1.21.0 2024-07-07 10:44:00 +03:00
Bogdan
45d8a8a4e6 Minor fixes and cover link for SubsPlease 2024-07-06 22:12:36 +03:00
Bogdan
a4546c77ce Avoid invalid requests for Nebulance 2024-07-06 11:33:21 +03:00
Bogdan
d69bf6360a Fixed: (Nebulance) Improve searching by release names 2024-07-06 09:21:55 +03:00
Bogdan
da9ce5b5c3 New: Enable "Sync Anime Standard Format Search" by default for new Sonarr apps 2024-07-05 22:26:34 +03:00
Bogdan
e092098101 Minor improvements to season parsing from titles for AnimeBytes 2024-07-05 16:39:04 +03:00
Bogdan
1a89a79b74 Avoid NullRef for missing filelist and tags fields 2024-07-05 16:13:32 +03:00
Bogdan
cb6bf49922 New: (Nebulance) Improvements for season and episode searching 2024-07-05 12:42:51 +03:00
Bogdan
4bcaba0be0 Fixed: Trimming disabled logs database
(cherry picked from commit d5dff8e8d6301b661a713702e1c476705423fc4f)
2024-07-01 05:41:37 +03:00
Bogdan
220ef723c7 Bump version to 1.20.1 2024-06-30 07:22:49 +03:00
121 changed files with 2426 additions and 1621 deletions

View File

@@ -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:

View File

@@ -46,6 +46,10 @@ class StackedBarChart extends Component {
size: 14,
family: defaultFontFamily
}
},
tooltip: {
mode: 'index',
position: 'average'
}
}
},

View File

@@ -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);

View File

@@ -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');

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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')})`
});
}

View File

@@ -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 }));

View File

@@ -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

View File

@@ -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}>

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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}

View File

@@ -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;

View 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;

View File

@@ -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;
}

View File

@@ -10,6 +10,7 @@ interface CssExports {
'indexers': string;
'modalBody': string;
'modalFooter': string;
'notice': string;
'scroller': string;
}
export const cssExports: CssExports;

View File

@@ -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;

View 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;

View File

@@ -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);

View File

@@ -38,6 +38,7 @@ export interface IndexerField extends ModelBase {
interface Indexer extends ModelBase {
name: string;
definitionName: string;
description: string;
encoding: string;
language: string;

View File

@@ -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}

View File

@@ -1,4 +1,6 @@
.message {
composes: alert from '~Components/Alert.css';
margin-top: 10px;
margin-bottom: 30px;
text-align: center;

View File

@@ -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>
);
}

View File

@@ -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}

View File

@@ -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}>

View File

@@ -1,4 +1,6 @@
.message {
composes: alert from '~Components/Alert.css';
margin-top: 10px;
margin-bottom: 30px;
text-align: center;

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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
);
}

View File

@@ -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>
);
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
);
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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),
};
}
);

View File

@@ -1,5 +0,0 @@
function sortByName(a, b) {
return a.name.localeCompare(b.name);
}
export default sortByName;

View 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;

View 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;

View File

@@ -2,6 +2,7 @@ export interface IndexerStatsIndexer {
indexerId: number;
indexerName: string;
averageResponseTime: number;
averageGrabResponseTime: number;
numberOfQueries: number;
numberOfGrabs: number;
numberOfRssQueries: number;

View File

@@ -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",

View File

@@ -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());

View File

@@ -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; }

View File

@@ -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)

View File

@@ -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>();

View File

@@ -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);

View File

@@ -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)
{

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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)
{

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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())
{

View File

@@ -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; }

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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>())

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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()

View File

@@ -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
{

View File

@@ -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()

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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>();
}
}

View File

@@ -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);
}
}

View File

@@ -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="))
{

View File

@@ -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; }
}

View File

@@ -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>())

View File

@@ -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;
}
}
}

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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();

View File

@@ -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();

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -357,5 +357,9 @@
"CustomFilter": "مرشحات مخصصة",
"IndexerHDBitsSettingsMediums": "متوسط",
"GrabRelease": "انتزاع الإصدار",
"ProxyValidationBadRequest": "فشل اختبار الوكيل. رمز الحالة: {statusCode}"
"ProxyValidationBadRequest": "فشل اختبار الوكيل. رمز الحالة: {statusCode}",
"Script": "النصي",
"BuiltIn": "مدمج",
"PublishedDate": "تاريخ النشر",
"AllSearchResultsHiddenByFilter": "يتم إخفاء جميع النتائج بواسطة عامل التصفية المطبق"
}

View File

@@ -357,5 +357,9 @@
"IndexerHDBitsSettingsMediums": "Среден",
"CustomFilter": "Персонализирани филтри",
"GrabRelease": "Grab Release",
"ProxyValidationBadRequest": "Неуспешно тестване на прокси. Код на състоянието: {statusCode}"
"ProxyValidationBadRequest": "Неуспешно тестване на прокси. Код на състоянието: {statusCode}",
"BuiltIn": "Вграден",
"Script": "Сценарий",
"PublishedDate": "Дата на публикуване",
"AllSearchResultsHiddenByFilter": "Всички резултати са скрити от приложения филтър"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -524,5 +524,10 @@
"UseSsl": "Χρησιμοποιήστε SSL",
"CustomFilter": "Custom Φιλτρα",
"GrabRelease": "Πιάσε την απελευθέρωση",
"ProxyValidationBadRequest": "Αποτυχία δοκιμής διακομιστή μεσολάβησης. StatusCode: {statusCode}"
"ProxyValidationBadRequest": "Αποτυχία δοκιμής διακομιστή μεσολάβησης. StatusCode: {statusCode}",
"Script": "Γραφή",
"BuiltIn": "Ενσωματωμένο",
"PublishedDate": "Ημερομηνία δημοσίευσης",
"Redirected": "Διευθύνω πάλιν",
"AllSearchResultsHiddenByFilter": "Όλα τα αποτελέσματα αποκρύπτονται από το εφαρμοσμένο φίλτρο"
}

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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."
}

View File

@@ -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é"
}

View File

@@ -411,5 +411,12 @@
"IndexerHDBitsSettingsMediums": "בינוני",
"ProxyValidationBadRequest": "נכשל בדיקת ה- proxy. קוד קוד: {statusCode}",
"CustomFilter": "מסננים מותאמים אישית",
"GrabRelease": "שחרור תפוס"
"GrabRelease": "שחרור תפוס",
"BuiltIn": "נִבנָה בְּ",
"Script": "תַסרִיט",
"PublishedDate": "תאריך פרסום",
"AddCategory": "הוסף קטגוריה",
"ActiveApps": "אפליקציות פעילות",
"ActiveIndexers": "אינדקסרים פעילים",
"AllSearchResultsHiddenByFilter": "כל התוצאות מוסתרות על ידי המסנן שהוחל"
}

View File

@@ -356,5 +356,9 @@
"IndexerHDBitsSettingsMediums": "मध्यम",
"CustomFilter": "कस्टम फ़िल्टर",
"ProxyValidationBadRequest": "प्रॉक्सी का परीक्षण करने में विफल। स्थिति कोड: {statusCode}",
"GrabRelease": "पकड़ो रिलीज"
"GrabRelease": "पकड़ो रिलीज",
"BuiltIn": "में निर्मित",
"Script": "लिपि",
"PublishedDate": "प्रकाशित तिथि",
"AllSearchResultsHiddenByFilter": "सभी परिणाम लागू फ़िल्टर द्वारा छिपे हुए हैं"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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 dellapplicazione, 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 dellapplicazione, 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"
}

View File

@@ -357,5 +357,9 @@
"IndexerHDBitsSettingsMediums": "中",
"CustomFilter": "カスタムフィルター",
"ProxyValidationBadRequest": "プロキシのテストに失敗しました。 StatusCode{statusCode}",
"GrabRelease": "グラブリリース"
"GrabRelease": "グラブリリース",
"Script": "脚本",
"BuiltIn": "ビルトイン",
"PublishedDate": "公開日",
"AllSearchResultsHiddenByFilter": "すべての結果は、適用されたフィルターによって非表示になります"
}

View File

@@ -357,5 +357,8 @@
"IndexerHDBitsSettingsMediums": "매질",
"CustomFilter": "사용자 지정 필터",
"GrabRelease": "그랩 릴리스",
"ProxyValidationBadRequest": "프록시를 테스트하지 못했습니다. StatusCode : {statusCode}"
"ProxyValidationBadRequest": "프록시를 테스트하지 못했습니다. StatusCode : {statusCode}",
"BuiltIn": "내장",
"PublishedDate": "발행일",
"AllSearchResultsHiddenByFilter": "적용된 필터에 의해 모든 결과가 숨겨집니다."
}

View File

@@ -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