Compare commits

...

56 Commits

Author SHA1 Message Date
Bogdan
55fa1ec637 Small improvements to IndexerNoDefinitionCheck 2024-06-20 17:40:54 +03:00
Weblate
b27a3d8272 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Mariot Tsitoara <mariot@tsitoara.fr>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translation: Servarr/Prowlarr
2024-06-20 17:39:11 +03:00
Bogdan
089d450b46 Fixed: (M-Team-TP) New API path
Co-authored-by: Garfield69 <garfield69@outlook.com>
2024-06-20 16:57:54 +03:00
dependabot[bot]
358ac7434d Bump ws from 7.5.9 to 7.5.10
Bumps [ws](https://github.com/websockets/ws) from 7.5.9 to 7.5.10.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.9...7.5.10)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 20:47:47 +03:00
dependabot[bot]
9cd505fd8a Bump braces from 3.0.2 to 3.0.3
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 22:56:14 +03:00
Bogdan
20ac2687df New: Ignore inaccessible folders when getting folders
(cherry picked from commit a30e9da7672a202cb9e9188cf106afc34a5d0361)
2024-06-18 22:45:12 +03:00
Weblate
9f075c09a2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Felix <edivad94@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/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/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/
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/
Translation: Servarr/Prowlarr
2024-06-16 16:24:03 +03:00
Bogdan
3793538ba4 Use tooltip component for detailed error message 2024-06-15 23:27:04 +03:00
Bogdan
4c4b16d234 Fixed: (FileList) Parsing poorly padded IMDb Ids 2024-06-14 22:10:09 +03:00
Bogdan
f5790bec2e HD-Space switched to Cardigann YML 2024-06-13 12:26:52 +03:00
Bogdan
6c0d08de56 Fixed: Ignore case for name validation in providers
(cherry picked from commit 0edc5ba99a15c5f80305b387a053f35fc3f6e51b)
2024-06-11 07:49:17 +03:00
Mark McDowall
ba344756b1 Fixed: Improve error messaging if config file isn't formatted correctly
(cherry picked from commit 52b72925f9d42c896144dde3099dc19c397327b0)
2024-06-11 07:48:15 +03:00
Servarr
dfda86aca3 Automated API Docs update 2024-06-10 17:54:04 +03:00
Bogdan
df6f83ed69 Increase timeout for docs 2024-06-10 17:40:27 +03:00
Bogdan
218d92a1ac docs: use application specific to platform 2024-06-10 17:21:57 +03:00
Bogdan
df2b529d01 Bump Swashbuckle to 6.6.2 2024-06-10 17:00:46 +03:00
Bogdan
0ef42dbb4d Display downtime message for Nebulance 2024-06-09 14:27:27 +03:00
Weblate
1a428197b2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yi Cao <caoyi06@qq.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: r0bertreh <Robert.reh@live.de>
Co-authored-by: topnew <sznetim@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
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/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-06-03 09:14:00 +03:00
Bogdan
09d7983845 Bump version to 1.19.0 2024-06-02 05:33:28 +03:00
Bogdan
6e01f3187a New: (UI) Detailed error message for inner exception in indexers validation 2024-06-01 13:47:23 +03:00
Bogdan
468436b9f7 Fixed: Remove extraneous rate limiting for grabs
Co-authored-by: Michael Goodnow <mmgoodnow@gmail.com>

Closes #2140
2024-06-01 10:37:30 +03:00
Bogdan
76c288a6e4 Fixed: Authentication issues with Cardigann definitions having captcha
This mostly reverts 68b895d2ad where the cache key was changed to something more specific to avoid another issue with shared settings, but sadly this resulted in a new instance of CardigannRequestGenerator with null `landingResultDocument` failing the login.

Fixes #2139
2024-06-01 08:07:47 +03:00
Bogdan
f95f67a7ca New: (Cardigann) Bump minimum version to v10 2024-05-27 21:05:14 +03:00
Bogdan
11864247eb Bump Microsoft.NET.Test.Sdk and Polly 2024-05-25 02:31:44 +03:00
Bogdan
74509ea7c9 Fixed: (MyAnonamouse) Don't die when no results on paginated queries 2024-05-19 01:24:10 +03:00
Bogdan
948fe0a6dc Fixed: Trimming slashes from UrlBase when using environment variable 2024-05-18 19:12:37 +03:00
Bogdan
a4257cbcde Bump Npgsql to 7.0.7 2024-05-13 15:33:46 +03:00
Bogdan
2929c3c898 Bump version to 1.18.0 2024-05-12 16:23:24 +03:00
Weblate
2c5f2187c8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translation: Servarr/Prowlarr
2024-05-10 14:05:29 +03:00
Bogdan
401ef88971 Refactor PasswordInput to use type password 2024-05-10 13:59:52 +03:00
Bogdan
4fb3754048 Fixed: Text color for inputs on login page 2024-05-09 23:40:50 +03:00
Mark McDowall
596efe8fb0 New: Dark theme for login screen
(cherry picked from commit cae134ec7b331d1c906343716472f3d043614b2c)
2024-05-09 23:40:49 +03:00
Bogdan
076a4f2574 Fix class name for AppIndexerMapRepository 2024-05-09 23:15:05 +03:00
Servarr
9561371a47 Automated API Docs update 2024-05-08 03:20:28 +03:00
Mark McDowall
16254cf5f9 New: Option to select download client when multiple of the same type are configured
(cherry picked from commit 07f0fbf9a51d54e44681fd0f74df4e048bff561a)
2024-05-08 03:14:17 +03:00
Jared
649a03e5a0 New: Config file setting to disable log database (#2123)
Co-authored-by: sillock1 <jprest97@gmail.com>
2024-05-07 19:52:02 +03:00
Bogdan
dd21d9b521 Fixed: Allow decimals for Seed Ratio 2024-05-07 00:11:20 +03:00
Bogdan
68b895d2ad Fixed: Don't share settings for same cached definition in CardigannRequestGenerator 2024-05-06 18:22:54 +03:00
Weblate
634016ae1b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translation: Servarr/Prowlarr
2024-05-05 13:12:04 +03:00
Mark McDowall
83c6751847 Forward X-Forwarded-Host header
(cherry picked from commit 3fbe4361386e9fb8dafdf82ad9f00f02bec746cc)
2024-05-05 12:29:22 +03:00
Jared
04bb0c51b1 New: Optionally use Environment Variables for settings in config.xml
(cherry picked from commit d051dac12c8b797761a0d1f3b4aa84cff47ed13d)
2024-05-05 11:39:55 +03:00
Bogdan
d2e9621de9 Bump version to 1.17.2 2024-05-05 11:38:40 +03:00
Bogdan
cb673ddc42 New: Host column in history and more info 2024-05-02 22:35:20 +03:00
Bogdan
440618f2b6 Fixed: Initialize databases after app folder migrations
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-05-02 00:21:01 +03:00
Bruno Garcia
ae79d45664 Update Sentry SDK add features
Co-authored-by: Stefan Jandl <reg@bitfox.at>
(cherry picked from commit 6377c688fc7b35749d608bf62796446bb5bcb11b)
2024-05-02 00:07:58 +03:00
Bogdan
1877ccb513 Update Pull Request Labeler config for v5 2024-04-29 14:35:43 +03:00
Bogdan
b3098f2e4c Use newer Node.js task for in pipelines 2024-04-29 14:32:46 +03:00
Bogdan
3e0af062c1 Parameter binding for API requests 2024-04-29 01:24:57 +03:00
Bogdan
858f85c50d Fix translations for proxy validation 2024-04-28 23:49:59 +03:00
Uruk
938848be65 (ci): update action version 2024-04-28 11:42:07 -05:00
Bogdan
615f5899cc Fixed: (TorrentDay) Update base urls and MST
Co-authored-by: garfield69 <garfield69@outlook.com>
2024-04-28 14:57:30 +03:00
Mark McDowall
5a6b1313e8 Validate that folders in paths don't start or end with a space
(cherry picked from commit 316b5cbf75b45ef9a25f96ce1f2fbed25ad94296)
2024-04-28 13:16:08 +03:00
Mark McDowall
ab7debb34b Improve paths longer than 256 on Windows failing to hardlink
(cherry picked from commit a97fbcc40a6247bf59678425cf460588fd4dbecd)
2024-04-28 13:07:52 +03:00
Bogdan
eee21de795 Fixed: Handle download redirects to magnet links 2024-04-28 13:05:13 +03:00
Bogdan
15fabbe7d0 Bump version to 1.17.1 2024-04-28 12:50:04 +03:00
Weblate
6aef48c6e7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: maodun96 <435795439@qq.com>
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/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-04-27 21:15:01 +03:00
151 changed files with 2960 additions and 1471 deletions

28
.github/labeler.yml vendored
View File

@@ -1,19 +1,31 @@
'Area: API':
- src/Prowlarr.Api.V1/**/*
- changed-files:
- any-glob-to-any-file:
- src/Prowlarr.Api.V1/**/*
'Area: Db-migration':
- src/NzbDrone.Core/Datastore/Migration/*
- changed-files:
- any-glob-to-any-file:
- src/NzbDrone.Core/Datastore/Migration/*
'Area: Download Clients':
- src/NzbDrone.Core/Download/Clients/**/*
- changed-files:
- any-glob-to-any-file:
- src/NzbDrone.Core/Download/Clients/**/*
'Area: Indexer':
- src/NzbDrone.Core/Indexers/**/*
- changed-files:
- any-glob-to-any-file:
- src/NzbDrone.Core/Indexers/**/*
'Area: Notifications':
- src/NzbDrone.Core/Notifications/**/*
- changed-files:
- any-glob-to-any-file:
- src/NzbDrone.Core/Notifications/**/*
'Area: UI':
- frontend/**/*
- package.json
- yarn.lock
- changed-files:
- any-glob-to-any-file:
- frontend/**/*
- package.json
- yarn.lock

View File

@@ -9,4 +9,4 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5

View File

@@ -9,7 +9,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: '90'

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.17.0'
majorVersion: '1.19.0'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
@@ -166,10 +166,10 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: NodeTool@0
- task: UseNode@1
displayName: Set Node.js version
inputs:
versionSpec: $(nodeVersion)
version: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1
@@ -1075,10 +1075,10 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: NodeTool@0
- task: UseNode@1
displayName: Set Node.js version
inputs:
versionSpec: $(nodeVersion)
version: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1

16
docs.sh Normal file → Executable file
View File

@@ -1,3 +1,7 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
PLATFORM=$1
if [ "$PLATFORM" = "Windows" ]; then
@@ -21,17 +25,23 @@ slnFile=src/Prowlarr.sln
platform=Posix
if [ "$PLATFORM" = "Windows" ]; then
application=Prowlarr.Console.dll
else
application=Prowlarr.dll
fi
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 &
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
sleep 30
sleep 45
kill %1

View File

@@ -12,11 +12,10 @@ function App({ store, history }) {
<DocumentTitle title={window.Prowlarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
<ApplyTheme />
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ConnectedRouter>
</Provider>
</DocumentTitle>

View File

@@ -1,50 +0,0 @@
import PropTypes from 'prop-types';
import React, { Fragment, useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.theme || window.Prowlarr.theme,
(
theme
) => {
return {
theme
};
}
);
}
function ApplyTheme({ theme, children }) {
// Update the CSS Variables
const updateCSSVariables = useCallback(() => {
const arrayOfVariableKeys = Object.keys(themes[theme]);
const arrayOfVariableValues = Object.values(themes[theme]);
// Loop through each array key and set the CSS Variables
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
// Based on our snippet from MDN
document.documentElement.style.setProperty(
`--${cssVariableKey}`,
arrayOfVariableValues[index]
);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables(theme);
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
ApplyTheme.propTypes = {
theme: PropTypes.string.isRequired,
children: PropTypes.object.isRequired
};
export default connect(createMapStateToProps)(ApplyTheme);

View File

@@ -0,0 +1,37 @@
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
import AppState from './State/AppState';
interface ApplyThemeProps {
children: ReactNode;
}
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
(theme) => {
return theme;
}
);
}
function ApplyTheme({ children }: ApplyThemeProps) {
const theme = useSelector(createThemeSelector());
const updateCSSVariables = useCallback(() => {
Object.entries(themes[theme]).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables();
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
export default ApplyTheme;

View File

@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
render() {
const {
className,
titleClassName,
descriptionClassName,
title,
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
} = this.props;
return (
<div>
<div className={className}>
<DescriptionListItemTitle
className={titleClassName}
>
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
}
DescriptionListItem.propTypes = {
className: PropTypes.string,
titleClassName: PropTypes.string,
descriptionClassName: PropTypes.string,
title: PropTypes.string,

View File

@@ -1,3 +1,7 @@
.validationFailures {
margin-bottom: 20px;
}
.details {
margin-left: 5px;
}

View File

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

View File

@@ -1,7 +1,9 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import { kinds } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import styles from './Form.css';
function Form(props) {
@@ -26,6 +28,18 @@ function Form(props) {
kind={kinds.DANGER}
>
{error.errorMessage}
{
error.detailedDescription ?
<Tooltip
className={styles.details}
anchor={<Icon name={icons.INFO} />}
tooltip={error.detailedDescription}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/> :
null
}
</Alert>
);
})
@@ -39,6 +53,18 @@ function Form(props) {
kind={kinds.WARNING}
>
{warning.errorMessage}
{
warning.detailedDescription ?
<Tooltip
className={styles.details}
anchor={<Icon name={icons.INFO} />}
tooltip={warning.detailedDescription}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/> :
null
}
</Alert>
);
})

View File

@@ -256,6 +256,7 @@ FormInputGroup.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.any,
values: PropTypes.arrayOf(PropTypes.any),
isFloat: PropTypes.bool,
type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all),
min: PropTypes.number,

View File

@@ -1,5 +0,0 @@
.input {
composes: input from '~Components/Form/TextInput.css';
font-family: $passwordFamily;
}

View File

@@ -1,7 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import TextInput from './TextInput';
import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input
function onCopy(e) {
@@ -13,17 +11,14 @@ function PasswordInput(props) {
return (
<TextInput
{...props}
type="password"
onCopy={onCopy}
/>
);
}
PasswordInput.propTypes = {
className: PropTypes.string.isRequired
};
PasswordInput.defaultProps = {
className: styles.input
...TextInput.props
};
export default PasswordInput;

View File

@@ -25,14 +25,3 @@
font-family: 'Ubuntu Mono';
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
}
/*
* text-security-disc
*/
@font-face {
font-weight: normal;
font-style: normal;
font-family: 'text-security-disc';
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
}

View File

@@ -0,0 +1,7 @@
enum DownloadProtocol {
Unknown = 'unknown',
Usenet = 'usenet',
Torrent = 'torrent',
}
export default DownloadProtocol;

View File

@@ -43,6 +43,7 @@ import {
faChevronCircleRight as fasChevronCircleRight,
faChevronCircleUp as fasChevronCircleUp,
faCircle as fasCircle,
faCircleDown as fasCircleDown,
faCloud as fasCloud,
faCloudDownloadAlt as fasCloudDownloadAlt,
faCog as fasCog,
@@ -141,6 +142,7 @@ export const CHECK_INDETERMINATE = fasMinus;
export const CHECK_CIRCLE = fasCheckCircle;
export const CHECK_SQUARE = fasSquareCheck;
export const CIRCLE = fasCircle;
export const CIRCLE_DOWN = fasCircleDown;
export const CIRCLE_OUTLINE = farCircle;
export const CLEAR = fasTrashAlt;
export const CLIPBOARD = fasCopy;

View File

@@ -21,6 +21,7 @@ function HistoryDetails(props) {
limit,
offset,
source,
host,
url
} = data;
@@ -86,6 +87,15 @@ function HistoryDetails(props) {
null
}
{
data ?
<DescriptionListItem
title={translate('Host')}
data={host}
/> :
null
}
{
data ?
<DescriptionListItem

View File

@@ -331,6 +331,21 @@ class HistoryRow extends Component {
);
}
if (name === 'host') {
return (
<TableRowCell
key={name}
className={styles.indexer}
>
{
data.host ?
data.host :
null
}
</TableRowCell>
);
}
if (name === 'elapsedTime') {
return (
<TableRowCell

View File

@@ -224,6 +224,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
name="seedRatio"
value={seedRatio}
helpText={translate('SeedRatioHelpText')}
isFloat={true}
onChange={onInputChange}
/>
</FormGroup>

View File

@@ -47,3 +47,42 @@ $hoverScale: 1.05;
right: 0;
white-space: nowrap;
}
.downloadLink {
composes: link from '~Components/Link/Link.css';
margin: 0 2px;
width: 22px;
color: var(--textColor);
text-align: center;
}
.manualDownloadContent {
position: relative;
display: inline-block;
margin: 0 2px;
width: 22px;
height: 20.39px;
vertical-align: middle;
line-height: 20.39px;
&:hover {
color: var(--iconButtonHoverColor);
}
}
.interactiveIcon {
position: absolute;
top: 4px;
left: 0;
/* width: 100%; */
text-align: center;
}
.downloadIcon {
position: absolute;
top: 7px;
left: 8px;
/* width: 100%; */
text-align: center;
}

View File

@@ -4,9 +4,13 @@ interface CssExports {
'actions': string;
'container': string;
'content': string;
'downloadIcon': string;
'downloadLink': string;
'indexerRow': string;
'info': string;
'infoRow': string;
'interactiveIcon': string;
'manualDownloadContent': string;
'title': string;
'titleRow': string;
}

View File

@@ -1,234 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons, kinds } from 'Helpers/Props';
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
import CategoryLabel from 'Search/Table/CategoryLabel';
import Peers from 'Search/Table/Peers';
import dimensions from 'Styles/Variables/dimensions';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import styles from './SearchIndexOverview.css';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
function getContentHeight(rowHeight, isSmallScreen) {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
return rowHeight - padding;
}
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed, grabError) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddedToDownloadClient');
} else if (grabError) {
return grabError;
}
return translate('AddToDownloadClient');
}
class SearchIndexOverview extends Component {
//
// Listeners
onGrabPress = () => {
const {
guid,
indexerId,
onGrabPress
} = this.props;
onGrabPress({
guid,
indexerId
});
};
//
// Render
render() {
const {
title,
infoUrl,
protocol,
downloadUrl,
magnetUrl,
categories,
seeders,
leechers,
indexerFlags,
size,
age,
ageHours,
ageMinutes,
indexer,
rowHeight,
isSmallScreen,
isGrabbed,
isGrabbing,
grabError
} = this.props;
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.info} style={{ height: contentHeight }}>
<div className={styles.titleRow}>
<div className={styles.title}>
<Link
to={infoUrl}
title={title}
>
<TextTruncate
line={2}
text={title}
/>
</Link>
</div>
<div className={styles.actions}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={this.onGrabPress}
/>
{
downloadUrl || magnetUrl ?
<IconButton
name={icons.SAVE}
title={translate('Save')}
to={downloadUrl ?? magnetUrl}
/> :
null
}
</div>
</div>
<div className={styles.indexerRow}>
{indexer}
</div>
<div className={styles.infoRow}>
<ProtocolLabel
protocol={protocol}
/>
{
protocol === 'torrent' &&
<Peers
seeders={seeders}
leechers={leechers}
/>
}
<Label>
{formatBytes(size)}
</Label>
<Label>
{formatAge(age, ageHours, ageMinutes)}
</Label>
<CategoryLabel
categories={categories}
/>
{
indexerFlags.length ?
indexerFlags
.sort((a, b) => a.localeCompare(b))
.map((flag, index) => {
return (
<Label key={index} kind={kinds.INFO}>
{titleCase(flag)}
</Label>
);
}) :
null
}
</div>
</div>
</div>
</div>
);
}
}
SearchIndexOverview.propTypes = {
guid: PropTypes.string.isRequired,
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
ageHours: PropTypes.number.isRequired,
ageMinutes: PropTypes.number.isRequired,
publishDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
infoUrl: PropTypes.string.isRequired,
downloadUrl: PropTypes.string,
magnetUrl: PropTypes.string,
indexerId: PropTypes.number.isRequired,
indexer: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
files: PropTypes.number,
grabs: PropTypes.number,
seeders: PropTypes.number,
leechers: PropTypes.number,
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
rowHeight: PropTypes.number.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onGrabPress: PropTypes.func.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string
};
SearchIndexOverview.defaultProps = {
isGrabbing: false,
isGrabbed: false
};
export default SearchIndexOverview;

View File

@@ -0,0 +1,262 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import TextTruncate from 'react-text-truncate';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons, kinds } from 'Helpers/Props';
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
import { IndexerCategory } from 'Indexer/Indexer';
import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal';
import CategoryLabel from 'Search/Table/CategoryLabel';
import Peers from 'Search/Table/Peers';
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
import dimensions from 'Styles/Variables/dimensions';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import styles from './SearchIndexOverview.css';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(
dimensions.movieIndexColumnPaddingSmallScreen
);
function getDownloadIcon(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddedToDownloadClient');
} else if (grabError) {
return grabError;
}
return translate('AddToDownloadClient');
}
interface SearchIndexOverviewProps {
guid: string;
protocol: DownloadProtocol;
age: number;
ageHours: number;
ageMinutes: number;
publishDate: string;
title: string;
infoUrl: string;
downloadUrl?: string;
magnetUrl?: string;
indexerId: number;
indexer: string;
categories: IndexerCategory[];
size: number;
seeders?: number;
leechers?: number;
indexerFlags: string[];
isGrabbing: boolean;
isGrabbed: boolean;
grabError?: string;
longDateFormat: string;
timeFormat: string;
rowHeight: number;
isSmallScreen: boolean;
onGrabPress(...args: unknown[]): void;
}
function SearchIndexOverview(props: SearchIndexOverviewProps) {
const {
guid,
indexerId,
protocol,
categories,
age,
ageHours,
ageMinutes,
publishDate,
title,
infoUrl,
downloadUrl,
magnetUrl,
indexer,
size,
seeders,
leechers,
indexerFlags = [],
isGrabbing = false,
isGrabbed = false,
grabError,
longDateFormat,
timeFormat,
rowHeight,
isSmallScreen,
onGrabPress,
} = props;
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
const { items: downloadClients } = useSelector(
createEnabledDownloadClientsSelector(protocol)
);
const onGrabPressWrapper = useCallback(() => {
onGrabPress({
guid,
indexerId,
});
}, [guid, indexerId, onGrabPress]);
const onOverridePress = useCallback(() => {
setIsOverrideModalOpen(true);
}, [setIsOverrideModalOpen]);
const onOverrideModalClose = useCallback(() => {
setIsOverrideModalOpen(false);
}, [setIsOverrideModalOpen]);
const contentHeight = useMemo(() => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
return rowHeight - padding;
}, [rowHeight, isSmallScreen]);
return (
<>
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.info} style={{ height: contentHeight }}>
<div className={styles.titleRow}>
<div className={styles.title}>
<Link to={infoUrl} title={title}>
<TextTruncate line={2} text={title} />
</Link>
</div>
<div className={styles.actions}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
{downloadClients.length > 1 ? (
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadClient')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={11}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={9}
/>
</div>
</Link>
) : null}
{downloadUrl || magnetUrl ? (
<IconButton
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
to={downloadUrl ?? magnetUrl}
/>
) : null}
</div>
</div>
<div className={styles.indexerRow}>{indexer}</div>
<div className={styles.infoRow}>
<ProtocolLabel protocol={protocol} />
{protocol === 'torrent' && (
<Peers seeders={seeders} leechers={leechers} />
)}
<Label>{formatBytes(size)}</Label>
<Label
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
includeSeconds: true,
})}
>
{formatAge(age, ageHours, ageMinutes)}
</Label>
<CategoryLabel categories={categories} />
{indexerFlags.length
? indexerFlags
.sort((a, b) => a.localeCompare(b))
.map((flag, index) => {
return (
<Label key={index} kind={kinds.INFO}>
{titleCase(flag)}
</Label>
);
})
: null}
</div>
</div>
</div>
</div>
<OverrideMatchModal
isOpen={isOverrideModalOpen}
title={title}
indexerId={indexerId}
guid={guid}
protocol={protocol}
isGrabbing={isGrabbing}
grabError={grabError}
onModalClose={onOverrideModalClose}
/>
</>
);
}
export default SearchIndexOverview;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { sizes } from 'Helpers/Props';
import SelectDownloadClientModalContent from './SelectDownloadClientModalContent';
interface SelectDownloadClientModalProps {
isOpen: boolean;
protocol: DownloadProtocol;
modalTitle: string;
onDownloadClientSelect(downloadClientId: number): void;
onModalClose(): void;
}
function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } =
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
<SelectDownloadClientModalContent
protocol={protocol}
modalTitle={modalTitle}
onDownloadClientSelect={onDownloadClientSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectDownloadClientModal;

View File

@@ -0,0 +1,74 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { kinds } from 'Helpers/Props';
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
import translate from 'Utilities/String/translate';
import SelectDownloadClientRow from './SelectDownloadClientRow';
interface SelectDownloadClientModalContentProps {
protocol: DownloadProtocol;
modalTitle: string;
onDownloadClientSelect(downloadClientId: number): void;
onModalClose(): void;
}
function SelectDownloadClientModalContent(
props: SelectDownloadClientModalContentProps
) {
const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props;
const { isFetching, isPopulated, error, items } = useSelector(
createEnabledDownloadClientsSelector(protocol)
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('SelectDownloadClientModalTitle', { modalTitle })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('DownloadClientsLoadError')}
</Alert>
) : null}
{isPopulated && !error ? (
<Form>
{items.map((downloadClient) => {
const { id, name, priority } = downloadClient;
return (
<SelectDownloadClientRow
key={id}
id={id}
name={name}
priority={priority}
onDownloadClientSelect={onDownloadClientSelect}
/>
);
})}
</Form>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectDownloadClientModalContent;

View File

@@ -0,0 +1,6 @@
.downloadClient {
display: flex;
justify-content: space-between;
padding: 8px;
border-bottom: 1px solid var(--borderColor);
}

View File

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

View File

@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import Link from 'Components/Link/Link';
import translate from 'Utilities/String/translate';
import styles from './SelectDownloadClientRow.css';
interface SelectSeasonRowProps {
id: number;
name: string;
priority: number;
onDownloadClientSelect(downloadClientId: number): unknown;
}
function SelectDownloadClientRow(props: SelectSeasonRowProps) {
const { id, name, priority, onDownloadClientSelect } = props;
const onSeasonSelectWrapper = useCallback(() => {
onDownloadClientSelect(id);
}, [id, onDownloadClientSelect]);
return (
<Link
className={styles.downloadClient}
component="div"
onPress={onSeasonSelectWrapper}
>
<div>{name}</div>
<div>{translate('PrioritySettings', { priority })}</div>
</Link>
);
}
export default SelectDownloadClientRow;

View File

@@ -0,0 +1,17 @@
.link {
composes: link from '~Components/Link/Link.css';
width: 100%;
}
.placeholder {
display: inline-block;
margin: -2px 0;
width: 100%;
outline: 2px dashed var(--dangerColor);
outline-offset: -2px;
}
.optional {
outline: 2px dashed var(--gray);
}

View File

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

View File

@@ -0,0 +1,35 @@
import classNames from 'classnames';
import React from 'react';
import Link from 'Components/Link/Link';
import styles from './OverrideMatchData.css';
interface OverrideMatchDataProps {
value?: string | number | JSX.Element | JSX.Element[];
isDisabled?: boolean;
isOptional?: boolean;
onPress: () => void;
}
function OverrideMatchData(props: OverrideMatchDataProps) {
const { value, isDisabled = false, isOptional, onPress } = props;
return (
<Link className={styles.link} isDisabled={isDisabled} onPress={onPress}>
{(value == null || (Array.isArray(value) && value.length === 0)) &&
!isDisabled ? (
<span
className={classNames(
styles.placeholder,
isOptional && styles.optional
)}
>
&nbsp;
</span>
) : (
value
)}
</Link>
);
}
export default OverrideMatchData;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { sizes } from 'Helpers/Props';
import OverrideMatchModalContent from './OverrideMatchModalContent';
interface OverrideMatchModalProps {
isOpen: boolean;
title: string;
indexerId: number;
guid: string;
protocol: DownloadProtocol;
isGrabbing: boolean;
grabError?: string;
onModalClose(): void;
}
function OverrideMatchModal(props: OverrideMatchModalProps) {
const {
isOpen,
title,
indexerId,
guid,
protocol,
isGrabbing,
grabError,
onModalClose,
} = props;
return (
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
<OverrideMatchModalContent
title={title}
indexerId={indexerId}
guid={guid}
protocol={protocol}
isGrabbing={isGrabbing}
grabError={grabError}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default OverrideMatchModal;

View File

@@ -0,0 +1,49 @@
.label {
composes: label from '~Components/Label.css';
cursor: pointer;
}
.item {
display: block;
margin-bottom: 5px;
margin-left: 50px;
}
.footer {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
display: flex;
justify-content: space-between;
overflow: hidden;
}
.error {
margin-right: 20px;
color: var(--dangerColor);
word-break: break-word;
}
.buttons {
display: flex;
}
@media only screen and (max-width: $breakpointSmall) {
.item {
margin-left: 0;
}
.footer {
display: block;
}
.error {
margin-right: 0;
margin-bottom: 10px;
}
.buttons {
justify-content: space-between;
flex-grow: 1;
}
}

View File

@@ -0,0 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'buttons': string;
'error': string;
'footer': string;
'item': string;
'label': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,150 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { grabRelease } from 'Store/Actions/releaseActions';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
import translate from 'Utilities/String/translate';
import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal';
import OverrideMatchData from './OverrideMatchData';
import styles from './OverrideMatchModalContent.css';
type SelectType = 'select' | 'downloadClient';
interface OverrideMatchModalContentProps {
indexerId: number;
title: string;
guid: string;
protocol: DownloadProtocol;
isGrabbing: boolean;
grabError?: string;
onModalClose(): void;
}
function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
const modalTitle = translate('ManualGrab');
const {
indexerId,
title,
guid,
protocol,
isGrabbing,
grabError,
onModalClose,
} = props;
const [downloadClientId, setDownloadClientId] = useState<number | null>(null);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
);
const previousIsGrabbing = usePrevious(isGrabbing);
const dispatch = useDispatch();
const { items: downloadClients } = useSelector(
createEnabledDownloadClientsSelector(protocol)
);
const onSelectModalClose = useCallback(() => {
setSelectModalOpen(null);
}, [setSelectModalOpen]);
const onSelectDownloadClientPress = useCallback(() => {
setSelectModalOpen('downloadClient');
}, [setSelectModalOpen]);
const onDownloadClientSelect = useCallback(
(downloadClientId: number) => {
setDownloadClientId(downloadClientId);
setSelectModalOpen(null);
},
[setDownloadClientId, setSelectModalOpen]
);
const onGrabPress = useCallback(() => {
dispatch(
grabRelease({
indexerId,
guid,
downloadClientId,
})
);
}, [indexerId, guid, downloadClientId, dispatch]);
useEffect(() => {
if (!isGrabbing && previousIsGrabbing) {
onModalClose();
}
}, [isGrabbing, previousIsGrabbing, onModalClose]);
useEffect(
() => {
dispatch(fetchDownloadClients());
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('OverrideGrabModalTitle', { title })}
</ModalHeader>
<ModalBody>
<DescriptionList>
{downloadClients.length > 1 ? (
<DescriptionListItem
className={styles.item}
title={translate('DownloadClient')}
data={
<OverrideMatchData
value={
downloadClients.find(
(downloadClient) => downloadClient.id === downloadClientId
)?.name ?? translate('Default')
}
onPress={onSelectDownloadClientPress}
/>
}
/>
) : null}
</DescriptionList>
</ModalBody>
<ModalFooter className={styles.footer}>
<div className={styles.error}>{grabError}</div>
<div className={styles.buttons}>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isGrabbing}
error={grabError}
onPress={onGrabPress}
>
{translate('GrabRelease')}
</SpinnerErrorButton>
</div>
</ModalFooter>
<SelectDownloadClientModal
isOpen={selectModalOpen === 'downloadClient'}
protocol={protocol}
modalTitle={modalTitle}
onDownloadClientSelect={onDownloadClientSelect}
onModalClose={onSelectModalClose}
/>
</ModalContent>
);
}
export default OverrideMatchModalContent;

View File

@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import withScrollPosition from 'Components/withScrollPosition';
import { bulkGrabReleases, cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions';
import { fetchDownloadClients } from 'Store/Actions/Settings/downloadClients';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector';
import SearchIndex from './SearchIndex';
@@ -55,12 +56,20 @@ function createMapDispatchToProps(dispatch, props) {
dispatchClearReleases() {
dispatch(clearReleases());
},
dispatchFetchDownloadClients() {
dispatch(fetchDownloadClients());
}
};
}
class SearchIndexConnector extends Component {
componentDidMount() {
this.props.dispatchFetchDownloadClients();
}
componentWillUnmount() {
this.props.dispatchCancelFetchReleases();
this.props.dispatchClearReleases();
@@ -85,6 +94,7 @@ SearchIndexConnector.propTypes = {
onBulkGrabPress: PropTypes.func.isRequired,
dispatchCancelFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired,
items: PropTypes.arrayOf(PropTypes.object)
};

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { executeCommand } from 'Store/Actions/commandActions';
function createReleaseSelector() {
return createSelector(
@@ -37,10 +36,6 @@ function createMapStateToProps() {
);
}
const mapDispatchToProps = {
dispatchExecuteCommand: executeCommand
};
class SearchIndexItemConnector extends Component {
//
@@ -71,4 +66,4 @@ SearchIndexItemConnector.propTypes = {
component: PropTypes.elementType.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
export default connect(createMapStateToProps, null)(SearchIndexItemConnector);

View File

@@ -67,3 +67,33 @@
color: var(--textColor);
}
.manualDownloadContent {
position: relative;
display: inline-block;
margin: 0 2px;
width: 22px;
height: 20.39px;
vertical-align: middle;
line-height: 20.39px;
&:hover {
color: var(--iconButtonHoverColor);
}
}
.interactiveIcon {
position: absolute;
top: 4px;
left: 0;
/* width: 100%; */
text-align: center;
}
.downloadIcon {
position: absolute;
top: 7px;
left: 8px;
/* width: 100%; */
text-align: center;
}

View File

@@ -6,12 +6,15 @@ interface CssExports {
'category': string;
'cell': string;
'checkInput': string;
'downloadIcon': string;
'downloadLink': string;
'externalLinks': string;
'files': string;
'grabs': string;
'indexer': string;
'indexerFlags': string;
'interactiveIcon': string;
'manualDownloadContent': string;
'peers': string;
'protocol': string;
'size': string;

View File

@@ -1,431 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import CategoryLabel from './CategoryLabel';
import Peers from './Peers';
import ReleaseLinks from './ReleaseLinks';
import styles from './SearchIndexRow.css';
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed, grabError) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddedToDownloadClient');
} else if (grabError) {
return grabError;
}
return translate('AddToDownloadClient');
}
class SearchIndexRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isConfirmGrabModalOpen: false
};
}
//
// Listeners
onGrabPress = () => {
const {
guid,
indexerId,
onGrabPress
} = this.props;
onGrabPress({
guid,
indexerId
});
};
onSavePress = () => {
const {
downloadUrl,
fileName,
onSavePress
} = this.props;
onSavePress({
downloadUrl,
fileName
});
};
//
// Render
render() {
const {
guid,
protocol,
downloadUrl,
magnetUrl,
categories,
age,
ageHours,
ageMinutes,
publishDate,
title,
infoUrl,
indexer,
size,
files,
grabs,
seeders,
leechers,
imdbId,
tmdbId,
tvdbId,
tvMazeId,
indexerFlags,
columns,
isGrabbing,
isGrabbed,
grabError,
longDateFormat,
timeFormat,
isSelected,
onSelectedChange
} = this.props;
return (
<>
{
columns.map((column) => {
const {
isVisible
} = column;
if (!isVisible) {
return null;
}
if (column.name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={guid}
key={column.name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (column.name === 'protocol') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<ProtocolLabel
protocol={protocol}
/>
</VirtualTableRowCell>
);
}
if (column.name === 'age') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
title={formatDateTime(publishDate, longDateFormat, timeFormat, { includeSeconds: true })}
>
{formatAge(age, ageHours, ageMinutes)}
</VirtualTableRowCell>
);
}
if (column.name === 'sortTitle') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<Link
to={infoUrl}
title={title}
>
<div>
{title}
</div>
</Link>
</VirtualTableRowCell>
);
}
if (column.name === 'indexer') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{indexer}
</VirtualTableRowCell>
);
}
if (column.name === 'size') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{formatBytes(size)}
</VirtualTableRowCell>
);
}
if (column.name === 'files') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{files}
</VirtualTableRowCell>
);
}
if (column.name === 'grabs') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{grabs}
</VirtualTableRowCell>
);
}
if (column.name === 'peers') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{
protocol === 'torrent' &&
<Peers
seeders={seeders}
leechers={leechers}
/>
}
</VirtualTableRowCell>
);
}
if (column.name === 'category') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<CategoryLabel
categories={categories}
/>
</VirtualTableRowCell>
);
}
if (column.name === 'indexerFlags') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{
!!indexerFlags.length &&
<Popover
anchor={
<Icon
name={icons.FLAG}
kind={kinds.PRIMARY}
/>
}
title={translate('IndexerFlags')}
body={
<ul>
{
indexerFlags.map((flag, index) => {
return (
<li key={index}>
{titleCase(flag)}
</li>
);
})
}
</ul>
}
position={tooltipPositions.LEFT}
/>
}
</VirtualTableRowCell>
);
}
if (column.name === 'actions') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={this.onGrabPress}
/>
{
downloadUrl ?
<IconButton
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
onPress={this.onSavePress}
/> :
null
}
{
magnetUrl ?
<IconButton
className={styles.downloadLink}
name={icons.MAGNET}
title={translate('Open')}
to={magnetUrl}
/> :
null
}
{
imdbId || tmdbId || tvdbId || tvMazeId ? (
<Popover
anchor={
<Icon
className={styles.externalLinks}
name={icons.EXTERNAL_LINK}
size={12}
/>
}
title={translate('Links')}
body={
<ReleaseLinks
categories={categories}
imdbId={imdbId}
tmdbId={tmdbId}
tvdbId={tvdbId}
tvMazeId={tvMazeId}
/>
}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
) : null
}
</VirtualTableRowCell>
);
}
return null;
})
}
</>
);
}
}
SearchIndexRow.propTypes = {
guid: PropTypes.string.isRequired,
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
ageHours: PropTypes.number.isRequired,
ageMinutes: PropTypes.number.isRequired,
publishDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
fileName: PropTypes.string.isRequired,
infoUrl: PropTypes.string.isRequired,
downloadUrl: PropTypes.string,
magnetUrl: PropTypes.string,
indexerId: PropTypes.number.isRequired,
indexer: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
files: PropTypes.number,
grabs: PropTypes.number,
seeders: PropTypes.number,
leechers: PropTypes.number,
imdbId: PropTypes.number,
tmdbId: PropTypes.number,
tvdbId: PropTypes.number,
tvMazeId: PropTypes.number,
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onGrabPress: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
SearchIndexRow.defaultProps = {
isGrabbing: false,
isGrabbed: false
};
export default SearchIndexRow;

View File

@@ -0,0 +1,395 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column';
import Popover from 'Components/Tooltip/Popover';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
import { IndexerCategory } from 'Indexer/Indexer';
import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal';
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
import { SelectStateInputProps } from 'typings/props';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import CategoryLabel from './CategoryLabel';
import Peers from './Peers';
import ReleaseLinks from './ReleaseLinks';
import styles from './SearchIndexRow.css';
function getDownloadIcon(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddedToDownloadClient');
} else if (grabError) {
return grabError;
}
return translate('AddToDownloadClient');
}
interface SearchIndexRowProps {
guid: string;
protocol: DownloadProtocol;
age: number;
ageHours: number;
ageMinutes: number;
publishDate: string;
title: string;
fileName: string;
infoUrl: string;
downloadUrl?: string;
magnetUrl?: string;
indexerId: number;
indexer: string;
categories: IndexerCategory[];
size: number;
files?: number;
grabs?: number;
seeders?: number;
leechers?: number;
imdbId?: string;
tmdbId?: number;
tvdbId?: number;
tvMazeId?: number;
indexerFlags: string[];
isGrabbing: boolean;
isGrabbed: boolean;
grabError?: string;
longDateFormat: string;
timeFormat: string;
columns: Column[];
isSelected?: boolean;
onSelectedChange(result: SelectStateInputProps): void;
onGrabPress(...args: unknown[]): void;
onSavePress(...args: unknown[]): void;
}
function SearchIndexRow(props: SearchIndexRowProps) {
const {
guid,
indexerId,
protocol,
categories,
age,
ageHours,
ageMinutes,
publishDate,
title,
fileName,
infoUrl,
downloadUrl,
magnetUrl,
indexer,
size,
files,
grabs,
seeders,
leechers,
imdbId,
tmdbId,
tvdbId,
tvMazeId,
indexerFlags = [],
isGrabbing = false,
isGrabbed = false,
grabError,
longDateFormat,
timeFormat,
columns,
isSelected,
onSelectedChange,
onGrabPress,
onSavePress,
} = props;
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
const { items: downloadClients } = useSelector(
createEnabledDownloadClientsSelector(protocol)
);
const onGrabPressWrapper = useCallback(() => {
onGrabPress({
guid,
indexerId,
});
}, [guid, indexerId, onGrabPress]);
const onSavePressWrapper = useCallback(() => {
onSavePress({
downloadUrl,
fileName,
});
}, [downloadUrl, fileName, onSavePress]);
const onOverridePress = useCallback(() => {
setIsOverrideModalOpen(true);
}, [setIsOverrideModalOpen]);
const onOverrideModalClose = useCallback(() => {
setIsOverrideModalOpen(false);
}, [setIsOverrideModalOpen]);
return (
<>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={guid}
key={name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'protocol') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<ProtocolLabel protocol={protocol} />
</VirtualTableRowCell>
);
}
if (name === 'age') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
includeSeconds: true,
})}
>
{formatAge(age, ageHours, ageMinutes)}
</VirtualTableRowCell>
);
}
if (name === 'sortTitle') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<Link to={infoUrl} title={title}>
<div>{title}</div>
</Link>
</VirtualTableRowCell>
);
}
if (name === 'indexer') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{indexer}
</VirtualTableRowCell>
);
}
if (name === 'size') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{formatBytes(size)}
</VirtualTableRowCell>
);
}
if (name === 'files') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{files}
</VirtualTableRowCell>
);
}
if (name === 'grabs') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{grabs}
</VirtualTableRowCell>
);
}
if (name === 'peers') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{protocol === 'torrent' && (
<Peers seeders={seeders} leechers={leechers} />
)}
</VirtualTableRowCell>
);
}
if (name === 'category') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<CategoryLabel categories={categories} />
</VirtualTableRowCell>
);
}
if (name === 'indexerFlags') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{!!indexerFlags.length && (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={
<ul>
{indexerFlags.map((flag, index) => {
return <li key={index}>{titleCase(flag)}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
/>
)}
</VirtualTableRowCell>
);
}
if (name === 'actions') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
{downloadClients.length > 1 ? (
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadClient')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
) : null}
{downloadUrl ? (
<IconButton
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
onPress={onSavePressWrapper}
/>
) : null}
{magnetUrl ? (
<IconButton
className={styles.downloadLink}
name={icons.MAGNET}
title={translate('Open')}
to={magnetUrl}
/>
) : null}
{imdbId || tmdbId || tvdbId || tvMazeId ? (
<Popover
anchor={
<Icon
className={styles.externalLinks}
name={icons.EXTERNAL_LINK}
size={12}
/>
}
title={translate('Links')}
body={
<ReleaseLinks
categories={categories}
imdbId={imdbId}
tmdbId={tmdbId}
tvdbId={tvdbId}
tvMazeId={tvMazeId}
/>
}
position={tooltipPositions.TOP}
/>
) : null}
</VirtualTableRowCell>
);
}
return null;
})}
<OverrideMatchModal
isOpen={isOverrideModalOpen}
title={title}
indexerId={indexerId}
guid={guid}
protocol={protocol}
isGrabbing={isGrabbing}
grabError={grabError}
onModalClose={onOverrideModalClose}
/>
</>
);
}
export default SearchIndexRow;

View File

@@ -82,6 +82,12 @@ export const defaultState = {
isSortable: false,
isVisible: false
},
{
name: 'host',
label: () => translate('Host'),
isSortable: false,
isVisible: false
},
{
name: 'elapsedTime',
label: () => translate('ElapsedTime'),

View File

@@ -0,0 +1,22 @@
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';
export default function createEnabledDownloadClientsSelector(
protocol: DownloadProtocol
) {
return createSelector(
createSortedSectionSelector('settings.downloadClients', sortByName),
(downloadClients: DownloadClientAppState) => {
const { isFetching, isPopulated, error, items } = downloadClients;
const clients = items.filter(
(item) => item.protocol === protocol && item.enable
);
return { isFetching, isPopulated, error, items: clients };
}
);
}

View File

@@ -188,7 +188,7 @@ module.exports = {
// Charts
chartBackgroundColor: '#262626',
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','),
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','),
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',')
};

View File

@@ -2,7 +2,7 @@ import * as dark from './dark';
import * as light from './light';
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const auto = defaultDark ? { ...dark } : { ...light };
const auto = defaultDark ? dark : light;
export default {
auto,

View File

@@ -188,7 +188,7 @@ module.exports = {
// Charts
chartBackgroundColor: '#fff',
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','),
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','),
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',')
};

View File

@@ -2,7 +2,6 @@ module.exports = {
// Families
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
passwordFamily: 'text-security-disc',
// Sizes
extraSmallFontSize: '11px',

View File

@@ -7,10 +7,10 @@ function formatBytes(input) {
return '';
}
return filesize(size, {
return `${filesize(size, {
base: 2,
round: 1
});
})}`;
}
export default formatBytes;

View File

@@ -57,8 +57,8 @@
<style>
body {
background-color: #f5f7fa;
color: #656565;
background-color: var(--pageBackground);
color: var(--textColor);
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
sans-serif;
}
@@ -88,14 +88,14 @@
padding: 10px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
background-color: #464b51;
background-color: var(--themeDarkColor);
}
.panel-body {
padding: 20px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fff;
background-color: var(--panelBackground);
}
.sign-in {
@@ -112,16 +112,18 @@
padding: 6px 16px;
width: 100%;
height: 35px;
border: 1px solid #dde6e9;
background-color: var(--inputBackgroundColor);
border: 1px solid var(--inputBorderColor);
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor);
color: var(--textColor);
}
.form-input:focus {
outline: 0;
border-color: #66afe9;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(102, 175, 233, 0.6);
border-color: var(--inputFocusBorderColor);
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
0 0 8px var(--inputFocusBoxShadowColor);
}
.button {
@@ -130,10 +132,10 @@
padding: 10px 0;
width: 100%;
border: 1px solid;
border-color: #5899eb;
border-color: var(--primaryBorderColor);
border-radius: 4px;
background-color: #5d9cec;
color: #fff;
background-color: var(--primaryBackgroundColor);
color: var(--white);
vertical-align: middle;
text-align: center;
white-space: nowrap;
@@ -141,9 +143,9 @@
}
.button:hover {
border-color: #3483e7;
background-color: #4b91ea;
color: #fff;
border-color: var(--primaryHoverBorderColor);
background-color: var(--primaryHoverBackgroundColor);
color: var(--white);
text-decoration: none;
}
@@ -165,24 +167,24 @@
.forgot-password {
margin-left: auto;
color: #909fa7;
color: var(--forgotPasswordColor);
text-decoration: none;
font-size: 13px;
}
.forgot-password:focus,
.forgot-password:hover {
color: #748690;
color: var(--forgotPasswordAltColor);
text-decoration: underline;
}
.forgot-password:visited {
color: #748690;
color: var(--forgotPasswordAltColor);
}
.login-failed {
margin-top: 20px;
color: #f05050;
color: var(--failedColor);
font-size: 14px;
}
@@ -291,5 +293,59 @@
loginFailedDiv.classList.remove("hidden");
}
var light = {
white: '#fff',
pageBackground: '#f5f7fa',
textColor: '#515253',
themeDarkColor: '#464b51',
panelBackground: '#fff',
inputBackgroundColor: '#fff',
inputBorderColor: '#dde6e9',
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
inputFocusBorderColor: '#66afe9',
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7',
failedColor: '#f05050',
forgotPasswordColor: '#909fa7',
forgotPasswordAltColor: '#748690'
};
var dark = {
white: '#fff',
pageBackground: '#202020',
textColor: '#ccc',
themeDarkColor: '#494949',
panelBackground: '#111',
inputBackgroundColor: '#333',
inputBorderColor: '#dde6e9',
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
inputFocusBorderColor: '#66afe9',
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7',
failedColor: '#f05050',
forgotPasswordColor: '#737d83',
forgotPasswordAltColor: '#546067'
};
var theme = "_THEME_";
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
dark :
light;
Object.entries(finalTheme).forEach(([key, value]) => {
document.documentElement.style.setProperty(
`--${key}`,
value
);
});
</script>
</html>

View File

@@ -1,4 +1,5 @@
export interface UiSettings {
theme: 'auto' | 'dark' | 'light';
showRelativeDates: boolean;
shortDateFormat: string;
longDateFormat: string;

View File

@@ -30,8 +30,8 @@
"@fortawesome/react-fontawesome": "0.2.0",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@sentry/browser": "7.100.0",
"@sentry/integrations": "7.100.0",
"@types/node": "18.19.31",
"@types/react": "18.2.79",
"@types/react-dom": "18.2.25",

View File

@@ -99,20 +99,52 @@
<RootNamespace Condition="'$(ProwlarrProject)'=='true'">$(MSBuildProjectName.Replace('Prowlarr','NzbDrone'))</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
<!-- FXCop Built into Net5 SDK now as NETAnalyzers, Enabled by default on net5 projects -->
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<ItemGroup Condition="'$(TestProject)'!='true'">
<!-- Annotates .NET assemblies with repository information including SHA -->
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<!-- Sentry specific configuration: Only in Release mode -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
<!-- OrgSlug, ProjectSlug and AuthToken are required.
They can be set below, via argument to 'msbuild -p:' or environment variable -->
<SentryOrg></SentryOrg>
<SentryProject></SentryProject>
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
without the need to deploy the application with PDBs -->
<SentryUploadSymbols>true</SentryUploadSymbols>
<!-- Source Link settings -->
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
<EmbedAllSources>true</EmbedAllSources>
</PropertyGroup>
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
</ItemGroup>
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net6.0'">
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
</ItemGroup>
<PropertyGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
<!-- FXCop Built into Net5 SDK now as NETAnalyzers, Enabled by default on net5 projects -->
<EnableNETAnalyzers>false</EnableNETAnalyzers>
</PropertyGroup>
<!-- Set up stylecop -->
<ItemGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'!='false'">
<!-- StyleCop analysis -->

View File

@@ -1,10 +1,12 @@
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
using NzbDrone.Test.Common;
@@ -43,6 +45,26 @@ namespace NzbDrone.Common.Test
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
.Callback<string, string>((p, t) => _configFileContents = t);
Mocker.GetMock<IOptions<AuthOptions>>()
.Setup(v => v.Value)
.Returns(new AuthOptions());
Mocker.GetMock<IOptions<AppOptions>>()
.Setup(v => v.Value)
.Returns(new AppOptions());
Mocker.GetMock<IOptions<ServerOptions>>()
.Setup(v => v.Value)
.Returns(new ServerOptions());
Mocker.GetMock<IOptions<LogOptions>>()
.Setup(v => v.Value)
.Returns(new LogOptions());
Mocker.GetMock<IOptions<UpdateOptions>>()
.Setup(v => v.Value)
.Returns(new UpdateOptions());
}
[Test]

View File

@@ -4,6 +4,7 @@ using System.Linq;
using FluentAssertions;
using NLog;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Sentry;
using NzbDrone.Test.Common;
@@ -26,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[SetUp]
public void Setup()
{
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
}
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)

View File

@@ -3,6 +3,7 @@ using System.IO;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Test.Common;
@@ -34,7 +35,7 @@ namespace NzbDrone.Common.Test
[TestCase(@"\\Testserver\\Test\", @"\\Testserver\Test")]
[TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")]
[TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")]
[TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")]
[TestCase(@"\\Testserver\Test\file.ext ", @"\\Testserver\Test\file.ext")]
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
public void Clean_Path_Windows(string dirty, string clean)
{
@@ -315,5 +316,30 @@ namespace NzbDrone.Common.Test
result[2].Should().Be(@"TV");
result[3].Should().Be(@"Series Title");
}
[TestCase(@"C:\Test\")]
[TestCase(@"C:\Test")]
[TestCase(@"C:\Test\TV\")]
[TestCase(@"C:\Test\TV")]
public void IsPathValid_should_be_true(string path)
{
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeTrue();
}
[TestCase(@"C:\Test \")]
[TestCase(@"C:\Test ")]
[TestCase(@"C:\ Test\")]
[TestCase(@"C:\ Test")]
[TestCase(@"C:\Test \TV")]
[TestCase(@"C:\ Test\TV")]
[TestCase(@"C:\Test \TV\")]
[TestCase(@"C:\ Test\TV\")]
[TestCase(@" C:\Test\TV\")]
[TestCase(@" C:\Test\TV")]
public void IsPathValid_should_be_false(string path)
{
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
}
}
}

View File

@@ -10,6 +10,7 @@ using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle;
@@ -29,10 +30,16 @@ namespace NzbDrone.Common.Test
.AddNzbDroneLogger()
.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddDummyDatabase()
.AddDummyLogDatabase()
.AddStartupContext(new StartupContext("first", "second"));
container.RegisterInstance(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<AppOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<AuthOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<ServerOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<LogOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<UpdateOptions>>().Object);
var serviceProvider = container.GetServiceProvider();

View File

@@ -153,7 +153,11 @@ namespace NzbDrone.Common.Disk
{
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return Directory.EnumerateDirectories(path);
return Directory.EnumerateDirectories(path, "*", new EnumerationOptions
{
AttributesToSkip = FileAttributes.System,
IgnoreInaccessible = true
});
}
public IEnumerable<string> GetFiles(string path, bool recursive)

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat;
@@ -28,6 +29,12 @@ namespace NzbDrone.Common.Extensions
public static string CleanFilePath(this string path)
{
if (path.IsNotNullOrWhiteSpace())
{
// Trim trailing spaces before checking if the path is valid so validation doesn't fail for something we can fix.
path = path.TrimEnd(' ');
}
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
@@ -36,10 +43,10 @@ namespace NzbDrone.Common.Extensions
// UNC
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
{
return info.FullName.TrimEnd('/', '\\', ' ');
return info.FullName.TrimEnd('/', '\\');
}
return info.FullName.TrimEnd('/').Trim('\\', ' ');
return info.FullName.TrimEnd('/').Trim('\\');
}
public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
@@ -143,6 +150,23 @@ namespace NzbDrone.Common.Extensions
return false;
}
if (path.Trim() != path)
{
return false;
}
var directoryInfo = new DirectoryInfo(path);
while (directoryInfo != null)
{
if (directoryInfo.Name.Trim() != directoryInfo.Name)
{
return false;
}
directoryInfo = directoryInfo.Parent;
}
if (validationType == PathValidationType.AnyOs)
{
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
@@ -253,6 +277,11 @@ namespace NzbDrone.Common.Extensions
return processName;
}
public static string CleanPath(this string path)
{
return Path.Join(path.Split(Path.DirectorySeparatorChar).Select(s => s.Trim()).ToArray());
}
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
{
return appFolderInfo.AppDataFolder;

View File

@@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
RegisterDebugger();
}
RegisterSentry(updateApp);
RegisterSentry(updateApp, appFolderInfo);
if (updateApp)
{
@@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
LogManager.ReconfigExistingLoggers();
}
private static void RegisterSentry(bool updateClient)
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
{
string dsn;
@@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
: "https://e38306161ff945999adf774a16e933c3@sentry.servarr.com/30";
}
var target = new SentryTarget(dsn)
var target = new SentryTarget(dsn, appFolderInfo)
{
Name = "sentryTarget",
Layout = "${message}"

View File

@@ -106,7 +106,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
public bool FilterEvents { get; set; }
public bool SentryEnabled { get; set; }
public SentryTarget(string dsn)
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
{
_sdk = SentrySdk.Init(o =>
{
@@ -114,9 +114,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200;
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
o.Environment = BuildInfo.Branch;
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
o.AutoSessionTracking = true;
// Caches files in the event device is offline
// Sentry creates a 'sentry' sub directory, no need to concat here
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
// default environment is production
if (!RuntimeInfo.IsProduction)
{
if (RuntimeInfo.IsDevelopment)
{
o.Environment = "development";
}
else if (RuntimeInfo.IsTesting)
{
o.Environment = "testing";
}
else
{
o.Environment = "other";
}
}
});
InitializeScope();
@@ -134,7 +158,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{
SentrySdk.ConfigureScope(scope =>
{
scope.User = new User
scope.User = new SentryUser
{
Id = HashUtil.AnonymousToken()
};
@@ -317,13 +341,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry
}
}
var level = LoggingLevelMap[logEvent.Level];
var sentryEvent = new SentryEvent(logEvent.Exception)
{
Level = LoggingLevelMap[logEvent.Level],
Level = level,
Logger = logEvent.LoggerName,
Message = logEvent.FormattedMessage
};
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
{
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
// the 'unhandled' exception flag
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
}
sentryEvent.SetExtras(extras);
sentryEvent.SetFingerprint(fingerPrint);

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Common.Options;
public class AppOptions
{
public string InstanceName { get; set; }
public string Theme { get; set; }
public bool? LaunchBrowser { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Common.Options;
public class AuthOptions
{
public string ApiKey { get; set; }
public bool? Enabled { get; set; }
public string Method { get; set; }
public string Required { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace NzbDrone.Common.Options;
public class LogOptions
{
public string Level { get; set; }
public bool? FilterSentryEvents { get; set; }
public int? Rotate { get; set; }
public bool? Sql { get; set; }
public string ConsoleLevel { get; set; }
public bool? AnalyticsEnabled { get; set; }
public string SyslogServer { get; set; }
public int? SyslogPort { get; set; }
public string SyslogLevel { get; set; }
public bool? DbEnabled { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace NzbDrone.Common.Options;
public class ServerOptions
{
public string UrlBase { get; set; }
public string BindAddress { get; set; }
public int? Port { get; set; }
public bool? EnableSsl { get; set; }
public int? SslPort { get; set; }
public string SslCertPath { get; set; }
public string SslCertPassword { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Common.Options;
public class UpdateOptions
{
public string Mechanism { get; set; }
public bool? Automatically { get; set; }
public string ScriptPath { get; set; }
public string Branch { get; set; }
}

View File

@@ -10,8 +10,8 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" />
<PackageReference Include="Npgsql" Version="7.0.6" />
<PackageReference Include="Sentry" Version="3.29.1" />
<PackageReference Include="Npgsql" Version="7.0.7" />
<PackageReference Include="Sentry" Version="4.0.2" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

View File

@@ -10,9 +10,9 @@ namespace NzbDrone.Core.Applications
void DeleteAllForApp(int appId);
}
public class TagRepository : BasicRepository<AppIndexerMap>, IAppIndexerMapRepository
public class AppIndexerMapRepository : BasicRepository<AppIndexerMap>, IAppIndexerMapRepository
{
public TagRepository(IMainDatabase database, IEventAggregator eventAggregator)
public AppIndexerMapRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}

View File

@@ -10,6 +10,7 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Datastore;
@@ -53,13 +54,14 @@ namespace NzbDrone.Core.Configuration
string SyslogServer { get; }
int SyslogPort { get; }
string SyslogLevel { get; }
bool LogDbEnabled { get; }
string Theme { get; }
string PostgresHost { get; }
int PostgresPort { get; }
string PostgresUser { get; }
string PostgresPassword { get; }
string PostgresMainDb { get; }
string PostgresLogDb { get; }
string Theme { get; }
}
public class ConfigFileProvider : IConfigFileProvider
@@ -72,6 +74,11 @@ namespace NzbDrone.Core.Configuration
private readonly IDiskProvider _diskProvider;
private readonly ICached<string> _cache;
private readonly PostgresOptions _postgresOptions;
private readonly AuthOptions _authOptions;
private readonly AppOptions _appOptions;
private readonly ServerOptions _serverOptions;
private readonly UpdateOptions _updateOptions;
private readonly LogOptions _logOptions;
private readonly string _configFile;
private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -82,13 +89,23 @@ namespace NzbDrone.Core.Configuration
ICacheManager cacheManager,
IEventAggregator eventAggregator,
IDiskProvider diskProvider,
IOptions<PostgresOptions> postgresOptions)
IOptions<PostgresOptions> postgresOptions,
IOptions<AuthOptions> authOptions,
IOptions<AppOptions> appOptions,
IOptions<ServerOptions> serverOptions,
IOptions<UpdateOptions> updateOptions,
IOptions<LogOptions> logOptions)
{
_cache = cacheManager.GetCache<string>(GetType());
_eventAggregator = eventAggregator;
_diskProvider = diskProvider;
_configFile = appFolderInfo.GetConfigPath();
_postgresOptions = postgresOptions.Value;
_authOptions = authOptions.Value;
_appOptions = appOptions.Value;
_serverOptions = serverOptions.Value;
_updateOptions = updateOptions.Value;
_logOptions = logOptions.Value;
}
public Dictionary<string, object> GetConfigDictionary()
@@ -144,7 +161,7 @@ namespace NzbDrone.Core.Configuration
{
const string defaultValue = "*";
var bindAddress = GetValue("BindAddress", defaultValue);
var bindAddress = _serverOptions.BindAddress ?? GetValue("BindAddress", defaultValue);
if (string.IsNullOrWhiteSpace(bindAddress))
{
return defaultValue;
@@ -154,19 +171,19 @@ namespace NzbDrone.Core.Configuration
}
}
public int Port => GetValueInt("Port", DEFAULT_PORT);
public int Port => _serverOptions.Port ?? GetValueInt("Port", DEFAULT_PORT);
public int SslPort => GetValueInt("SslPort", DEFAULT_SSL_PORT);
public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", DEFAULT_SSL_PORT);
public bool EnableSsl => GetValueBoolean("EnableSsl", false);
public bool EnableSsl => _serverOptions.EnableSsl ?? GetValueBoolean("EnableSsl", false);
public bool LaunchBrowser => GetValueBoolean("LaunchBrowser", true);
public bool LaunchBrowser => _appOptions.LaunchBrowser ?? GetValueBoolean("LaunchBrowser", true);
public string ApiKey
{
get
{
var apiKey = GetValue("ApiKey", GenerateApiKey());
var apiKey = _authOptions.ApiKey ?? GetValue("ApiKey", GenerateApiKey());
if (apiKey.IsNullOrWhiteSpace())
{
@@ -182,7 +199,7 @@ namespace NzbDrone.Core.Configuration
{
get
{
var enabled = GetValueBoolean("AuthenticationEnabled", false, false);
var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false);
if (enabled)
{
@@ -190,37 +207,45 @@ namespace NzbDrone.Core.Configuration
return AuthenticationType.Basic;
}
return GetValueEnum("AuthenticationMethod", AuthenticationType.None);
return Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
? enumValue
: GetValueEnum("AuthenticationMethod", AuthenticationType.None);
}
}
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
public AuthenticationRequiredType AuthenticationRequired =>
Enum.TryParse<AuthenticationRequiredType>(_authOptions.Required, out var enumValue)
? enumValue
: GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false);
// TODO: Change back to "master" for the first stable release.
public string Branch => GetValue("Branch", "master").ToLowerInvariant();
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "master").ToLowerInvariant();
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false);
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false);
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public string Theme => GetValue("Theme", "auto", persist: false);
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
public string SslCertPath => GetValue("SslCertPath", "");
public string SslCertPassword => GetValue("SslCertPassword", "");
public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false);
public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
public string UrlBase
{
get
{
var urlBase = GetValue("UrlBase", "").Trim('/');
var urlBase = (_serverOptions.UrlBase ?? GetValue("UrlBase", "")).Trim('/');
if (urlBase.IsNullOrWhiteSpace())
{
@@ -232,19 +257,36 @@ namespace NzbDrone.Core.Configuration
}
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
public string InstanceName => GetValue("InstanceName", BuildInfo.AppName);
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
public string InstanceName
{
get
{
var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
public UpdateMechanism UpdateMechanism => GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
if (instanceName.ContainsIgnoreCase(BuildInfo.AppName))
{
return instanceName;
}
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
return BuildInfo.AppName;
}
}
public string SyslogServer => GetValue("SyslogServer", "", persist: false);
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", false, false);
public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false);
public UpdateMechanism UpdateMechanism =>
Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue)
? enumValue
: GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
public string SyslogLevel => GetValue("SyslogLevel", LogLevel, false).ToLowerInvariant();
public string UpdateScriptPath => _updateOptions.ScriptPath ?? GetValue("UpdateScriptPath", "", false);
public string SyslogServer => _logOptions.SyslogServer ?? GetValue("SyslogServer", "", persist: false);
public int SyslogPort => _logOptions.SyslogPort ?? GetValueInt("SyslogPort", 514, persist: false);
public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
public int GetValueInt(string key, int defaultValue, bool persist = true)
{
@@ -277,13 +319,13 @@ namespace NzbDrone.Core.Configuration
return valueHolder.First().Value.Trim();
}
//Save the value
// Save the value
if (persist)
{
SetValue(key, defaultValue);
}
//return the default value
// return the default value
return defaultValue.ToString();
});
}
@@ -380,13 +422,21 @@ namespace NzbDrone.Core.Configuration
throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Prowlarr will recreate it.");
}
return XDocument.Parse(_diskProvider.ReadAllText(_configFile));
var xDoc = XDocument.Parse(_diskProvider.ReadAllText(_configFile));
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).ToList();
if (config.Count != 1)
{
throw new InvalidConfigFileException($"{_configFile} is invalid. Please delete the config file and Prowlarr will recreate it.");
}
return xDoc;
}
var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
var newXDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
newXDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
return xDoc;
return newXDoc;
}
}
catch (XmlException ex)

View File

@@ -8,6 +8,12 @@ namespace NzbDrone.Core.Datastore.Extensions
public static IContainer AddDatabase(this IContainer container)
{
container.RegisterDelegate<IDbFactory, IMainDatabase>(f => new MainDatabase(f.Create()), Reuse.Singleton);
return container;
}
public static IContainer AddLogDatabase(this IContainer container)
{
container.RegisterDelegate<IDbFactory, ILogDatabase>(f => new LogDatabase(f.Create(MigrationType.Log)), Reuse.Singleton);
return container;
@@ -16,6 +22,12 @@ namespace NzbDrone.Core.Datastore.Extensions
public static IContainer AddDummyDatabase(this IContainer container)
{
container.RegisterInstance<IMainDatabase>(new MainDatabase(null));
return container;
}
public static IContainer AddDummyLogDatabase(this IContainer container)
{
container.RegisterInstance<ILogDatabase>(new LogDatabase(null));
return container;

View File

@@ -1,10 +1,8 @@
using System;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers;
@@ -27,7 +25,6 @@ namespace NzbDrone.Core.Download
private readonly IDownloadClientStatusService _downloadClientStatusService;
private readonly IIndexerFactory _indexerFactory;
private readonly IIndexerStatusService _indexerStatusService;
private readonly IRateLimitService _rateLimitService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
@@ -35,7 +32,6 @@ namespace NzbDrone.Core.Download
IDownloadClientStatusService downloadClientStatusService,
IIndexerFactory indexerFactory,
IIndexerStatusService indexerStatusService,
IRateLimitService rateLimitService,
IEventAggregator eventAggregator,
Logger logger)
{
@@ -43,7 +39,6 @@ namespace NzbDrone.Core.Download
_downloadClientStatusService = downloadClientStatusService;
_indexerFactory = indexerFactory;
_indexerStatusService = indexerStatusService;
_rateLimitService = rateLimitService;
_eventAggregator = eventAggregator;
_logger = logger;
}
@@ -132,15 +127,7 @@ namespace NzbDrone.Core.Download
_logger.Trace("Attempting download of {0}", link);
var url = new Uri(link);
// Limit grabs to 2 per second.
if (link.IsNotNullOrWhiteSpace() && !link.StartsWith("magnet:"))
{
await _rateLimitService.WaitAndPulseAsync(url.Host, TimeSpan.FromSeconds(2));
}
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(indexerId));
var success = false;
var downloadedBytes = Array.Empty<byte>();
var release = new ReleaseInfo
{
@@ -151,12 +138,14 @@ namespace NzbDrone.Core.Download
DownloadProtocol = indexer.Protocol
};
var grabEvent = new IndexerDownloadEvent(release, success, source, host, release.Title, release.DownloadUrl)
var grabEvent = new IndexerDownloadEvent(release, false, source, host, release.Title, release.DownloadUrl)
{
Indexer = indexer,
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
};
byte[] downloadedBytes;
try
{
downloadedBytes = await indexer.Download(url);

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions.Cardigann;
@@ -9,12 +10,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
public class NoDefinitionCheck : HealthCheckBase
public class IndexerNoDefinitionCheck : HealthCheckBase
{
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;
private readonly IIndexerFactory _indexerFactory;
public NoDefinitionCheck(IIndexerDefinitionUpdateService indexerDefinitionUpdateService, IIndexerFactory indexerFactory, ILocalizationService localizationService)
public IndexerNoDefinitionCheck(IIndexerDefinitionUpdateService indexerDefinitionUpdateService, IIndexerFactory indexerFactory, ILocalizationService localizationService)
: base(localizationService)
{
_indexerDefinitionUpdateService = indexerDefinitionUpdateService;
@@ -23,23 +24,22 @@ namespace NzbDrone.Core.HealthCheck.Checks
public override HealthCheck Check()
{
var currentDefs = _indexerDefinitionUpdateService.All();
var currentDefinitions = _indexerDefinitionUpdateService.All();
var noDefinitionIndexers = _indexerFactory.AllProviders(false)
.Where(i => i.Definition.Implementation == "Cardigann" && currentDefinitions.All(d => d.File != ((CardigannSettings)i.Definition.Settings).DefinitionFile))
.ToList();
var noDefIndexers = _indexerFactory.AllProviders(false)
.Where(i => i.Definition.Implementation == "Cardigann" && currentDefs.All(d => d.File != ((CardigannSettings)i.Definition.Settings).DefinitionFile)).ToList();
if (noDefIndexers.Count == 0)
if (noDefinitionIndexers.Count == 0)
{
return new HealthCheck(GetType());
}
var healthType = HealthCheckResult.Error;
var healthMessage = string.Format(_localizationService.GetLocalizedString("IndexerNoDefCheckMessage"),
string.Join(", ", noDefIndexers.Select(v => v.Definition.Name)));
return new HealthCheck(GetType(),
healthType,
healthMessage,
HealthCheckResult.Error,
_localizationService.GetLocalizedString("IndexerNoDefinitionCheckHealthCheckMessage", new Dictionary<string, object>
{
{ "indexerNames", string.Join(", ", noDefinitionIndexers.Select(v => v.Definition.Name).ToArray()) }
}),
"#indexers-have-no-definition");
}

View File

@@ -190,16 +190,16 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
if (response.StatusCode != HttpStatusCode.OK)
{
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode)));
_logger.Error("Proxy validation failed: {0}", response.StatusCode);
failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationBadRequest", new Dictionary<string, object> { { "statusCode", response.StatusCode } })));
}
var result = JsonConvert.DeserializeObject<FlareSolverrResponse>(response.Content);
}
catch (Exception ex)
{
_logger.Error(ex, "Proxy Health Check failed");
failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url.Host)));
_logger.Error(ex, "Proxy validation failed");
failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationUnableToConnect", new Dictionary<string, object> { { "exceptionMessage", ex.Message } })));
}
return new ValidationResult(failures);

View File

@@ -41,14 +41,14 @@ namespace NzbDrone.Core.IndexerProxies
// We only care about 400 responses, other error codes can be ignored
if (response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy. StatusCode: {0}", response.StatusCode)));
_logger.Error("Proxy validation failed: {0}", response.StatusCode);
failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationBadRequest", new Dictionary<string, object> { { "statusCode", response.StatusCode } })));
}
}
catch (Exception ex)
{
_logger.Error(ex, "Proxy Health Check failed");
failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy: {0}", ex.Message)));
_logger.Error(ex, "Proxy validation failed");
failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationUnableToConnect", new Dictionary<string, object> { { "exceptionMessage", ex.Message } })));
}
return new ValidationResult(failures);

View File

@@ -29,7 +29,7 @@ namespace NzbDrone.Core.IndexerVersions
/* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 9;
private const int DEFINITION_VERSION = 10;
// Used when moving yml to C#
private readonly List<string> _definitionBlocklist = new ()

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
private readonly ICached<CardigannRequestGenerator> _generatorCache;
public override string Name => "Cardigann";
public override string[] IndexerUrls => new string[] { "" };
public override string[] IndexerUrls => new[] { "" };
public override string Description => "";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
@@ -58,10 +58,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
Settings = Settings
});
generator = (CardigannRequestGenerator)SetCookieFunctions(generator);
generator.Definition = Definition;
generator.Settings = Settings;
generator = (CardigannRequestGenerator)SetCookieFunctions(generator);
_generatorCache.ClearExpired();
return generator;

View File

@@ -337,9 +337,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
variables[name] = selected.Key;
break;
case "info":
variables[name] = value;
break;
case "info_cookie":
case "info_flaresolverr":
case "info_useragent":
case "cardigannCaptcha":
// no-op
break;
default:
throw new NotSupportedException($"Type {setting.Type} is not supported.");

View File

@@ -1178,14 +1178,14 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
if (method == HttpMethod.Get && searchUrls.Contains(searchUrl))
{
_logger.Trace("Skip duplicated request {0}", searchUrl);
_logger.Trace("Skip duplicated request for {0}: {1}", Definition.Name, searchUrl);
continue;
}
searchUrls.Add(searchUrl);
_logger.Debug($"Adding request: {searchUrl}");
_logger.Debug("Adding request for {0}: {1}", Definition.Name, searchUrl);
var requestBuilder = new HttpRequestBuilder(searchUrl)
{

View File

@@ -61,7 +61,7 @@ public class FileListParser : IParseIndexerResponse
var imdbId = 0;
if (row.ImdbId is { Length: > 2 })
{
imdbId = int.Parse(row.ImdbId.Substring(2));
int.TryParse(row.ImdbId.TrimStart('t'), out imdbId);
}
var downloadVolumeFactor = row.FreeLeech ? 0 : 1;

View File

@@ -20,6 +20,7 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions
{
[Obsolete("Moved to YML for Cardigann")]
public class HDSpace : TorrentIndexerBase<HDSpaceSettings>
{
public override string Name => "HD-Space";

View File

@@ -51,12 +51,12 @@ public class MTeamTp : TorrentIndexerBase<MTeamTpSettings>
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new MTeamTpRequestGenerator(Settings, Capabilities);
return new MTeamTpRequestGenerator(Settings, Capabilities, BuildApiUrl(Settings));
}
public override IParseIndexerResponse GetParser()
{
return new MTeamTpParser(Settings, Capabilities.Categories);
return new MTeamTpParser(Settings, Capabilities.Categories, BuildApiUrl(Settings));
}
public override async Task<byte[]> Download(Uri link)
@@ -168,19 +168,26 @@ public class MTeamTp : TorrentIndexerBase<MTeamTpSettings>
return caps;
}
private static string BuildApiUrl(IIndexerSettings settings)
{
return $"https://api.{settings.BaseUrl.AsSpan(settings.BaseUrl.IndexOf('.') + 1)}";
}
}
public class MTeamTpRequestGenerator : IIndexerRequestGenerator
{
private readonly MTeamTpSettings _settings;
private readonly IndexerCapabilities _capabilities;
private readonly string _apiUrl;
private readonly int[] _trackerAdultCategories = { 410, 429, 424, 430, 426, 437, 431, 432, 436, 425, 433, 411, 412, 413, 440 };
public MTeamTpRequestGenerator(MTeamTpSettings settings, IndexerCapabilities capabilities)
public MTeamTpRequestGenerator(MTeamTpSettings settings, IndexerCapabilities capabilities, string apiUrl)
{
_settings = settings;
_capabilities = capabilities;
_apiUrl = apiUrl;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
@@ -253,7 +260,7 @@ public class MTeamTpRequestGenerator : IIndexerRequestGenerator
private IndexerRequest BuildSearchRequest(MTeamTpRequestType requestType, IEnumerable<int> categoryMapping, string searchTerm, string imdbId)
{
var request = new HttpRequestBuilder(_settings.BaseUrl)
var request = new HttpRequestBuilder(_apiUrl)
.Resource("/api/torrent/search")
.SetHeader("x-api-key", _settings.ApiKey)
.Accept(HttpAccept.Json)
@@ -299,11 +306,13 @@ public class MTeamTpParser : IParseIndexerResponse
{
private readonly MTeamTpSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly string _apiUrl;
public MTeamTpParser(MTeamTpSettings settings, IndexerCapabilitiesCategories categories)
public MTeamTpParser(MTeamTpSettings settings, IndexerCapabilitiesCategories categories, string apiUrl)
{
_settings = settings;
_categories = categories;
_apiUrl = apiUrl;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
@@ -390,7 +399,7 @@ public class MTeamTpParser : IParseIndexerResponse
private string GetDownloadUrl(int torrentId)
{
var url = new HttpUri(_settings.BaseUrl)
var url = new HttpUri(_apiUrl)
.CombinePath("/api/torrent/genDlToken")
.AddQueryParam("id", torrentId);

View File

@@ -387,14 +387,14 @@ namespace NzbDrone.Core.Indexers.Definitions
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}");
}
var torrentInfos = new List<TorrentInfo>();
var releaseInfos = new List<ReleaseInfo>();
var jsonResponse = JsonConvert.DeserializeObject<MyAnonamouseResponse>(indexerResponse.Content);
var error = jsonResponse.Error;
if (error is "Nothing returned, out of 0" or "Nothing returned, out of 1")
if (error.IsNotNullOrWhiteSpace() && error.StartsWithIgnoreCase("Nothing returned, out of"))
{
return torrentInfos.ToArray();
return releaseInfos.ToArray();
}
var hasUserVip = HasUserVip();
@@ -462,10 +462,10 @@ namespace NzbDrone.Core.Indexers.Definitions
release.MinimumRatio = 1;
release.MinimumSeedTime = 259200; // 72 hours
torrentInfos.Add(release);
releaseInfos.Add(release);
}
return torrentInfos.ToArray();
return releaseInfos.ToArray();
}
private bool HasUserVip()

View File

@@ -228,10 +228,21 @@ namespace NzbDrone.Core.Indexers.Definitions
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request", indexerResponse.HttpResponse.StatusCode);
}
var jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse.Content);
JsonRpcResponse<NebulanceTorrents> jsonResponse;
try
{
jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse.Content);
}
catch (Exception ex)
{
STJson.TryDeserialize<JsonRpcResponse<string>>(indexerResponse.HttpResponse.Content, out var response);
throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", ex, response?.Result ?? ex.Message);
}
if (jsonResponse.Error != null || jsonResponse.Result == null)
{

View File

@@ -20,16 +20,21 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Name => "TorrentDay";
public override string[] IndexerUrls => new[]
{
"https://torrentday.cool/",
"https://tday.love/",
"https://torrentday.cool/",
"https://secure.torrentday.com/",
"https://classic.torrentday.com/",
"https://www.torrentday.com/",
"https://www.torrentday.me/",
"https://torrentday.it/",
"https://td.findnemo.net/",
"https://td.getcrazy.me/",
"https://td.venom.global/",
"https://td.workisboring.net/"
"https://td.workisboring.net/",
"https://tday.findnemo.net/",
"https://tday.getcrazy.me/",
"https://tday.venom.global/",
"https://tday.workisboring.net/"
};
public override string Description => "TorrentDay (TD) is a Private site for TV / MOVIES / GENERAL";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
@@ -266,7 +271,7 @@ namespace NzbDrone.Core.Indexers.Definitions
DownloadVolumeFactor = downloadMultiplier,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 172800 // 48 hours
MinimumSeedTime = 259200 // 72 hours
};
torrentInfos.Add(release);

View File

@@ -1,23 +1,34 @@
using NzbDrone.Common.Exceptions;
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Indexers.Exceptions
{
public class IndexerException : NzbDroneException
{
private readonly IndexerResponse _indexerResponse;
public IndexerException(IndexerResponse response, string message, params object[] args)
: base(message, args)
{
_indexerResponse = response;
}
public IndexerResponse Response { get; }
public IndexerException(IndexerResponse response, string message)
: base(message)
{
_indexerResponse = response;
Response = response;
}
public IndexerResponse Response => _indexerResponse;
public IndexerException(IndexerResponse response, string message, Exception innerException)
: base(message, innerException)
{
Response = response;
}
public IndexerException(IndexerResponse response, string message, params object[] args)
: base(message, args)
{
Response = response;
}
public IndexerException(IndexerResponse response, string message, Exception innerException, params object[] args)
: base(message, innerException, args)
{
Response = response;
}
}
}

View File

@@ -18,6 +18,7 @@ using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
using Polly;
using Polly.Retry;
@@ -27,6 +28,7 @@ namespace NzbDrone.Core.Indexers
where TSettings : IIndexerSettings, new()
{
protected const int MaxNumResultsPerQuery = 1000;
private const int MaxRedirects = 5;
protected readonly IIndexerHttpClient _httpClient;
protected readonly IEventAggregator _eventAggregator;
@@ -239,11 +241,47 @@ namespace NzbDrone.Core.Indexers
request.RateLimit = RateLimit;
}
request.AllowAutoRedirect = false;
byte[] fileData;
try
{
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
if (response.StatusCode is HttpStatusCode.MovedPermanently or HttpStatusCode.Found or HttpStatusCode.SeeOther)
{
var autoRedirectChain = new List<string> { request.Url.ToString() };
do
{
var redirectUrl = response.RedirectUrl;
_logger.Debug("Download request is being redirected to: {0}", redirectUrl);
if (redirectUrl.IsNullOrWhiteSpace())
{
throw new WebException("Remote website tried to redirect without providing a location.");
}
if (redirectUrl.StartsWith("magnet:"))
{
return await Download(new Uri(redirectUrl));
}
request.Url = new HttpUri(redirectUrl);
autoRedirectChain.Add(request.Url.ToString());
if (autoRedirectChain.Count > MaxRedirects)
{
throw new WebException($"Too many download redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError);
}
response = await _httpClient.ExecuteProxiedAsync(request, Definition);
}
while (response.StatusCode is HttpStatusCode.MovedPermanently or HttpStatusCode.Found or HttpStatusCode.SeeOther);
}
fileData = response.ResponseData;
_logger.Debug("Downloaded for release finished ({0} bytes from {1})", fileData.Length, link.AbsoluteUri);
@@ -287,10 +325,7 @@ namespace NzbDrone.Core.Indexers
protected virtual Task<HttpRequest> GetDownloadRequest(Uri link)
{
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri)
{
AllowAutoRedirect = FollowRedirect
};
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri);
if (Cookies != null)
{
@@ -780,7 +815,10 @@ namespace NzbDrone.Core.Indexers
{
_logger.Warn(ex, "Unable to connect to indexer");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, please check your DNS settings and ensure IPv6 is working or disabled. " + ex.Message);
return new NzbDroneValidationFailure(string.Empty, "Unable to connect to indexer, please check your DNS settings and ensure IPv6 is working or disabled. " + ex.Message)
{
DetailedDescription = ex.InnerException?.Message
};
}
catch (TaskCanceledException ex)
{
@@ -807,7 +845,10 @@ namespace NzbDrone.Core.Indexers
{
_logger.Warn(ex, "Unable to connect to indexer");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message);
return new NzbDroneValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message)
{
DetailedDescription = ex.InnerException?.Message
};
}
return null;

View File

@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Indexers
[FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsAppsMinimumSeeders", HelpText = "IndexerSettingsAppsMinimumSeedersHelpText", Advanced = true)]
public int? AppMinimumSeeders { get; set; }
[FieldDefinition(2, Type = FieldType.Textbox, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")]
[FieldDefinition(2, Type = FieldType.Number, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")]
public double? SeedRatio { get; set; }
[FieldDefinition(3, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)]

View File

@@ -355,5 +355,7 @@
"ResetAPIKeyMessageText": "هل أنت متأكد أنك تريد إعادة تعيين مفتاح API الخاص بك؟",
"RestartProwlarr": "أعد تشغيل {appName}",
"CustomFilter": "مرشحات مخصصة",
"IndexerHDBitsSettingsMediums": "متوسط"
"IndexerHDBitsSettingsMediums": "متوسط",
"GrabRelease": "انتزاع الإصدار",
"ProxyValidationBadRequest": "فشل اختبار الوكيل. رمز الحالة: {statusCode}"
}

View File

@@ -355,5 +355,7 @@
"ResetAPIKeyMessageText": "Наистина ли искате да нулирате своя API ключ?",
"RestartProwlarr": "Рестартирайте {appName}",
"IndexerHDBitsSettingsMediums": "Среден",
"CustomFilter": "Персонализирани филтри"
"CustomFilter": "Персонализирани филтри",
"GrabRelease": "Grab Release",
"ProxyValidationBadRequest": "Неуспешно тестване на прокси. Код на състоянието: {statusCode}"
}

View File

@@ -19,7 +19,7 @@
"ScriptPath": "Camí de l'script",
"Search": "Cerca",
"Files": "Fitxers",
"SettingsEnableColorImpairedModeHelpText": "Estil alternat per a permetre als usuaris amb problemes de color distingir millor la informació codificada per colors",
"SettingsEnableColorImpairedModeHelpText": "Estil modificat per a permetre als usuaris amb problemes visuals distingir millor la informació codificada per colors",
"TagIsNotUsedAndCanBeDeleted": "L'etiqueta no està en ús i es pot suprimir",
"TagsSettingsSummary": "Consulta totes les etiquetes i com s'utilitzen. Les etiquetes no utilitzades es poden eliminar",
"Tasks": "Tasques",
@@ -196,7 +196,7 @@
"MovieIndexScrollBottom": "Índex de pel·lícules: Desplaçament inferior",
"OnApplicationUpdate": "A l'actualitzar de l'aplicació",
"OnApplicationUpdateHelpText": "A l'actualitzar de l'aplicació",
"OnGrab": "Al capturar",
"OnGrab": "Al capturar llançament",
"PackageVersion": "Versió del paquet",
"ProxyFailedToTestHealthCheckMessage": "No s'ha pogut provar el servidor intermediari: {url}",
"ProxyResolveIpHealthCheckMessage": "No s'ha pogut resoldre l'adreça IP de l'amfitrió intermediari configurat {proxyHostName}",
@@ -216,7 +216,7 @@
"SettingsShowRelativeDatesHelpText": "Mostra dates relatives (avui/ahir/etc) o absolutes",
"ShowSearch": "Mostra la cerca",
"ShowSearchHelpText": "Mostra el botó de cerca al passar el cursor",
"Shutdown": "Tanca",
"Shutdown": "Apaga",
"Sort": "Ordena",
"Source": "Font",
"SSLCertPassword": "Contrasenya de certificat SSL",
@@ -240,7 +240,7 @@
"IndexerPriority": "Prioritat de l'indexador",
"UnsavedChanges": "Canvis no desats",
"UpdateAutomaticallyHelpText": "Baixeu i instal·leu les actualitzacions automàticament. Encara podreu instal·lar des de Sistema: Actualitzacions",
"UpdateStartupTranslocationHealthCheckMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici \"{startupFolder}\" es troba en una carpeta de translocació d'aplicacions.",
"UpdateStartupTranslocationHealthCheckMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici '{startupFolder}' es troba en una carpeta de translocació d'aplicacions.",
"UpdateUiNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari '{userName}' no pot escriure la carpeta de la IU '{uiFolder}'.",
"UpdateScriptPathHelpText": "Camí a un script personalitzat que pren un paquet d'actualització i gestiona la resta del procés d'actualització",
"Uptime": "Temps de funcionament",
@@ -294,62 +294,62 @@
"UnableToAddANewNotificationPleaseTryAgain": "No es pot afegir una notificació nova, torneu-ho a provar.",
"UnableToLoadGeneralSettings": "No es pot carregar la configuració general",
"UnableToLoadHistory": "No es pot carregar l'historial",
"UpdateStartupNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari \"{userName}\" no pot escriure la carpeta d'inici \"{startupFolder}\".",
"UpdateStartupNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari '{userName}' no té permisos d'escriptura de la carpeta d'inici '{startupFolder}'.",
"URLBase": "Base URL",
"Usenet": "Usenet",
"View": "Visualitza",
"Yesterday": "Ahir",
"ApplicationStatusCheckSingleClientMessage": "Llistes no disponibles a causa d'errors: {0}",
"ApplicationStatusCheckSingleClientMessage": "Aplicacions no disponibles a causa d'errors: {0}",
"AnalyticsEnabledHelpText": "Envieu informació anònima d'ús i errors als servidors de {appName}. Això inclou informació sobre el vostre navegador, quines pàgines {appName} WebUI feu servir, informes d'errors, així com el sistema operatiu i la versió de l'entorn d'execució. Utilitzarem aquesta informació per a prioritzar les funcions i les correccions d'errors.",
"HistoryCleanupDaysHelpTextWarning": "Els fitxers de la paperera de reciclatge més antics que el nombre de dies seleccionat es netejaran automàticament",
"UnableToAddANewAppProfilePleaseTryAgain": "No es pot afegir un perfil de qualitat nou, torneu-ho a provar.",
"UnableToAddANewAppProfilePleaseTryAgain": "No es pot afegir un perfil d'aplicació nou, torneu-ho a provar.",
"BackupFolderHelpText": "Els camins relatius estaran sota el directori AppData de {appName}",
"AllIndexersHiddenDueToFilter": "Totes les pel·lícules estan ocultes a causa del filtre aplicat.",
"AllIndexersHiddenDueToFilter": "Tots els indexadors estan ocults a causa del filtre aplicat.",
"EnableRss": "Activa RSS",
"Grabs": "Captura",
"EnableAutomaticSearchHelpText": "S'utilitzarà quan es realitzin cerques automàtiques mitjançant la interfície d'usuari o per {appName}",
"UnableToAddANewApplicationPleaseTryAgain": "No es pot afegir una notificació nova, torneu-ho a provar.",
"UnableToAddANewApplicationPleaseTryAgain": "No es pot afegir una aplicació nova, torneu-ho a provar.",
"Application": "Aplicacions",
"Applications": "Aplicacions",
"ApplicationStatusCheckAllClientMessage": "Totes les llistes no estan disponibles a causa d'errors",
"ApplicationStatusCheckAllClientMessage": "Totes les aplicacions no estan disponibles a causa d'errors",
"AuthenticationMethodHelpText": "Es requereix nom d'usuari i contrasenya per a accedir a {appName}",
"ApplicationLongTermStatusCheckAllClientMessage": "Tots els indexadors no estan disponibles a causa d'errors durant més de 6 hores",
"ApplicationLongTermStatusCheckSingleClientMessage": "Els indexadors no estan disponibles a causa d'errors durant més de 6 hores: {0}",
"ApplicationLongTermStatusCheckAllClientMessage": "Totes les aplicacions no estan disponibles a causa d'errors durant més de 6 hores",
"ApplicationLongTermStatusCheckSingleClientMessage": "Aplicacions no disponibles a causa d'errors durant més de 6 hores: {0}",
"BindAddressHelpText": "Adreça IP vàlida, localhost o '*' per a totes les interfícies",
"BranchUpdate": "Branca que s'utilitza per a actualitzar {appName}",
"Connect": "Notificacions",
"DeleteApplicationMessageText": "Esteu segur que voleu suprimir la notificació '{0}'?",
"DeleteIndexerProxyMessageText": "Esteu segur que voleu suprimir la llista '{0}'?",
"DeleteApplicationMessageText": "Esteu segur que voleu suprimir l'aplicació '{name}'?",
"DeleteIndexerProxyMessageText": "Esteu segur que voleu suprimir el servidor intermediari de l'indexador '{name}'?",
"Encoding": "Codificació",
"ForMoreInformationOnTheIndividualDownloadClients": "Per obtenir més informació sobre els clients de baixada individuals, feu clic als botons de més informació.",
"ForMoreInformationOnTheIndividualDownloadClients": "Per a obtenir més informació sobre els clients de baixada individuals, feu clic als botons de més informació.",
"GeneralSettingsSummary": "Port, SSL, nom d'usuari/contrasenya, servidor intermediari, analítiques i actualitzacions",
"GrabReleases": "Captura novetat",
"GrabReleases": "Captura llançament(s)",
"HistoryCleanupDaysHelpText": "Establiu a 0 per desactivar la neteja automàtica",
"Notification": "Notificacions",
"Notifications": "Notificacions",
"ReleaseBranchCheckOfficialBranchMessage": "La branca {0} no és una branca de llançament de {appName} vàlida, no rebreu actualitzacions",
"TagsHelpText": "S'aplica a pel·lícules amb almenys una etiqueta coincident",
"Torrent": "Torrent",
"UnableToAddANewIndexerProxyPleaseTryAgain": "No es pot afegir un indexador nou, torneu-ho a provar.",
"TagsHelpText": "S'aplica a indexadors amb almenys una etiqueta coincident",
"Torrent": "Torrents",
"UnableToAddANewIndexerProxyPleaseTryAgain": "No es pot afegir un servidor intermediari d'indexador nou, torneu-ho a provar.",
"UpdateMechanismHelpText": "Utilitzeu l'actualitzador integrat de {appName} o un script",
"UserAgentProvidedByTheAppThatCalledTheAPI": "Agent d'usuari proporcionat per l'aplicació per fer peticions a l'API",
"IndexerProxyStatusAllUnavailableHealthCheckMessage": "Tots els indexadors no estan disponibles a causa d'errors",
"IndexerProxyStatusUnavailableHealthCheckMessage": "Els indexadors no estan disponibles a causa d'errors: {indexerProxyNames}",
"LaunchBrowserHelpText": " Obriu un navegador web i navegueu a la pàgina d'inici de {appName} a l'inici de l'aplicació.",
"Link": "Enllaços",
"Link": "Enllaç",
"UILanguageHelpText": "Idioma que utilitzarà {appName} per a la interfície d'usuari",
"Remove": "Elimina",
"Replace": "Substitueix",
"TheLatestVersionIsAlreadyInstalled": "La darrera versió de {0} ja està instal·lada",
"ThemeHelpText": "Canvieu el tema de la interfície d'usuari de l'aplicació, el tema \"Automàtic\" utilitzarà el tema del vostre sistema operatiu per configurar el mode clar o fosc. Inspirat en Theme.Park",
"TheLatestVersionIsAlreadyInstalled": "La darrera versió de {appName} ja està instal·lada",
"ThemeHelpText": "Canvieu el tema de la interfície d'usuari de l'aplicació, el tema \"Automàtic\" utilitzarà el tema del vostre sistema operatiu per configurar el mode clar o fosc. Inspirat en {inspiredBy}.",
"ApplicationURL": "URL de l'aplicació",
"Publisher": "Editor",
"ApplicationUrlHelpText": "URL extern d'aquesta aplicació, inclòs http(s)://, port i URL base",
"ApplyTagsHelpTextAdd": "Afegeix: afegeix les etiquetes a la llista d'etiquetes existent",
"ApplyTagsHelpTextHowToApplyApplications": "Com aplicar etiquetes a les pel·lícules seleccionades",
"ApplicationUrlHelpText": "URL extern de l'aplicació, inclòs http(s)://, port i URL base",
"ApplyTagsHelpTextAdd": "Afegiment: afegeix les etiquetes a la llista d'etiquetes existent",
"ApplyTagsHelpTextHowToApplyApplications": "Com aplicar etiquetes a les aplicacions seleccionades",
"ApplyTagsHelpTextHowToApplyIndexers": "Com aplicar etiquetes als indexadors seleccionats",
"ApplyTagsHelpTextRemove": "Eliminar: elimina les etiquetes introduïdes",
"DeleteSelectedApplicationsMessageText": "Esteu segur que voleu suprimir l'indexador '{0}'?",
"ApplyTagsHelpTextRemove": "Eliminació: elimina les etiquetes introduïdes",
"DeleteSelectedApplicationsMessageText": "Esteu segur que voleu suprimir {count} sol·licituds seleccionades?",
"Label": "Etiqueta",
"ApplyTagsHelpTextReplace": "Substitució: substituïu les etiquetes per les etiquetes introduïdes (no introduïu cap etiqueta per a esborrar totes les etiquetes)",
"DeleteSelectedDownloadClients": "Suprimeix el(s) client(s) de baixada",
@@ -360,23 +360,23 @@
"More": "Més",
"Season": "Temporada",
"Theme": "Tema",
"Track": "Traça",
"Track": "Pista",
"Year": "Any",
"UpdateAvailableHealthCheckMessage": "Nova actualització disponible",
"ConnectionLostReconnect": "{appName} intentarà connectar-se automàticament, o podeu fer clic a recarregar.",
"ConnectionLostToBackend": "{appName} ha perdut la connexió amb el backend i s'haurà de tornar a carregar per a restaurar la funcionalitat.",
"RecentChanges": "Canvis recents",
"WhatsNew": "Que hi ha de nou?",
"minutes": "Minuts",
"DeleteAppProfileMessageText": "Esteu segur que voleu suprimir el perfil de qualitat {0}",
"NotificationStatusSingleClientHealthCheckMessage": "Llistes no disponibles a causa d'errors: {notificationNames}",
"WhatsNew": "Novetats",
"minutes": "minuts",
"DeleteAppProfileMessageText": "Esteu segur que voleu suprimir el perfil de l'aplicació '{name}'?",
"NotificationStatusSingleClientHealthCheckMessage": "Notificacions no disponibles a causa d'errors: {notificationNames}",
"AddConnection": "Afegeix una connexió",
"NotificationStatusAllClientHealthCheckMessage": "Totes les notificacions no estan disponibles a causa d'errors",
"AuthBasic": "Basic (finestra emergent del navegador)",
"AuthBasic": "Bàsic (finestra emergent del navegador)",
"AuthForm": "Formularis (pàgina d'inici de sessió)",
"DisabledForLocalAddresses": "Desactivat per a adreces locals",
"None": "Cap",
"ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau de l'API?",
"ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau API?",
"RestartProwlarr": "Reinicia {appName}",
"AuthenticationRequired": "Autenticació necessària",
"CountDownloadClientsSelected": "{count} client(s) de baixada seleccionat(s)",
@@ -412,15 +412,15 @@
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmeu la nova contrasenya",
"AuthenticationRequiredUsernameHelpTextWarning": "Introduïu un nom d'usuari nou",
"Categories": "Categories",
"ApiKeyValidationHealthCheckMessage": "Actualitzeu la vostra clau de l'API perquè tingui almenys {length} caràcters. Podeu fer-ho mitjançant la configuració o el fitxer de configuració",
"ApiKeyValidationHealthCheckMessage": "Actualitzeu la vostra clau API perquè tingui almenys {length} caràcters. Podeu fer-ho mitjançant la configuració o el fitxer de configuració",
"Episode": "Episodi",
"EditApplicationImplementation": "Edita la notificació - {implementationName}",
"EditApplicationImplementation": "Edita l'aplicació - {implementationName}",
"EditConnectionImplementation": "Afegeix una connexió - {implementationName}",
"EditIndexerProxyImplementation": "Edita l'indexador - {implementationName}",
"EditIndexerProxyImplementation": "Edita el servidor intermediari de l'indexador - {implementationName}",
"days": "dies",
"Album": "àlbum",
"Artist": "artista",
"AddApplicationImplementation": "Afegeix una condició - {implementationName}",
"Album": "Àlbum",
"Artist": "Artista",
"AddApplicationImplementation": "Afegeix una aplicació - {implementationName}",
"AddIndexerProxyImplementation": "Afegeix un indexador - {implementationName}",
"Category": "Categoria",
"Clone": "Clona",
@@ -431,7 +431,7 @@
"Id": "ID",
"Author": "Autor",
"ManageClients": "Gestiona els clients",
"CountApplicationsSelected": "{count} Col·lecció(ns) seleccionades",
"CountApplicationsSelected": "{count} aplicacions seleccionades",
"DownloadClientAriaSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada d'Aria2",
"Donate": "Dona",
"BlackholeFolderHelpText": "Carpeta on {appName} emmagatzemarà els fitxers {extension}",
@@ -439,9 +439,34 @@
"Directory": "Directori",
"DownloadClientDelugeSettingsUrlBaseHelpText": "Afegeix un prefix a l'url json del Deluge, vegeu {url}",
"CustomFilter": "Filtres personalitzats",
"IndexerHDBitsSettingsCodecs": "Còdec",
"DownloadClientSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {connectionName}, com ara {url}",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada d'Aria2",
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
"IndexerHDBitsSettingsMediums": "Suport"
"IndexerHDBitsSettingsCodecs": "Còdecs",
"DownloadClientSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {clientName}, com ara {url}",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Transmission",
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de rTorrent",
"IndexerHDBitsSettingsMediums": "Mitjans",
"Query": "Consulta",
"AddApplication": "Afegeix una aplicació",
"AddRemoveOnly": "Només afegeix i elimina",
"AddSyncProfile": "Afegeix perfil de sincronització",
"Auth": "Autenticació",
"ActiveIndexers": "Indexadors actius",
"Description": "Descripció",
"DeleteSelectedIndexers": "Suprimeix els indexadors seleccionats",
"IndexerSettingsBaseUrlHelpText": "Seleccioneu quina URL base utilitzarà {appName} per a les sol·licituds al lloc",
"ActiveApps": "Aplicacions actives",
"AddCategory": "Afegeix una categoria",
"AddDownloadClientToProwlarr": "Afegir un client de baixada permet que {appName} enviï publicacions directament des de la interfície d'usuari mentre fa una cerca manual.",
"AddIndexerProxy": "Afegeix un servidor intermediari per a l'indexador",
"AddNewIndexer": "Afegeix un nou indexador",
"AddToDownloadClient": "Afegeix llançament al client de baixades",
"AddedToDownloadClient": "Baixada afegida al client",
"DevelopmentSettings": "Configuració de desenvolupament",
"Mixed": "Combinat",
"Apps": "Aplicacions",
"Privacy": "Privacitat",
"Redirect": "Redirecció",
"Stats": "Estadístiques",
"Private": "Privat",
"Proxies": "Servidors intermediaris",
"Public": "Públic"
}

View File

@@ -412,5 +412,8 @@
"IndexerHDBitsSettingsCodecs": "Kodek",
"IndexerHDBitsSettingsMediums": "Střední",
"Directory": "Adresář",
"CustomFilter": "Vlastní filtry"
"CustomFilter": "Vlastní filtry",
"ProxyValidationBadRequest": "Nepodařilo se otestovat proxy. StatusCode: {statusCode}",
"Default": "Výchozí",
"GrabRelease": "Uchopte uvolnění"
}

View File

@@ -392,5 +392,7 @@
"EditApplicationImplementation": "Tilføj forbindelse - {implementationName}",
"EditDownloadClientImplementation": "Tilføj downloadklient - {implementationName}",
"IndexerHDBitsSettingsMediums": "Medium",
"CustomFilter": "Bruger Tilpassede Filtere"
"CustomFilter": "Bruger Tilpassede Filtere",
"ProxyValidationBadRequest": "Kunne ikke teste proxy. Statuskode: {statusCode}",
"GrabRelease": "Grab Release"
}

View File

@@ -105,7 +105,7 @@
"Discord": "Discord",
"Docker": "Docker",
"Donations": "Spenden",
"DownloadClient": "Download Client",
"DownloadClient": "Downloader",
"DownloadClientSettings": "Downloader Einstellungen",
"DownloadClientStatusAllClientHealthCheckMessage": "Alle Download Clients sind aufgrund von Fehlern nicht verfügbar",
"DownloadClientStatusSingleClientHealthCheckMessage": "Download Clients aufgrund von Fehlern nicht verfügbar: {downloadClientNames}",
@@ -179,7 +179,7 @@
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Alle Indexer sind wegen über 6 Stunden langen bestehender Fehler nicht verfügbar",
"IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexer wegen über 6 Stunden langen bestehenden Fehlern nicht verfügbar: {indexerNames}",
"IndexerName": "Indexer-Name",
"IndexerNoDefCheckMessage": "Indexer haben keine Definition und werden nicht funktionieren: {0}. Bitte entferne und (oder) füge diese neu zu {appName} hinzu",
"IndexerNoDefinitionCheckHealthCheckMessage": "Indexer haben keine Definition und werden nicht funktionieren: {0}. Bitte entferne und (oder) füge diese neu zu {appName} hinzu",
"IndexerObsoleteCheckMessage": "Indexer sind nicht mehr verfügbar oder wurden aktualiiert: {0}. Bitte enfernen und (oder) neu zu {appName} hinzufügen",
"IndexerPriority": "Priorität",
"IndexerPriorityHelpText": "Indexer Priorität von 1 (höchste) bis 50 (niedrigste). Standard: 25.",
@@ -546,5 +546,64 @@
"CountApplicationsSelected": "{count} Ausgewählte Sammlung(en)",
"DownloadClientAriaSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads. Lassen Sie das Feld leer, um den standardmäßigen rTorrent-Speicherort zu verwenden",
"ManageClients": "Verwalte Clienten",
"BlackholeFolderHelpText": "Ordner, in dem {appName} die Datei {extension} speichert"
"BlackholeFolderHelpText": "Ordner, in dem {appName} die Datei {extension} speichert",
"DownloadClientSettingsInitialStateHelpText": "Anfangszustand für zu {clientName} hinzugefügte Torrents",
"DownloadClientSettingsDestinationHelpText": "Legt das Ziel für den Download manuell fest; lassen Sie es leer, um die Standardeinstellung zu verwenden",
"DownloadClientSettingsUrlBaseHelpText": "Fügt ein Präfix zur {clientName} Url hinzu, z.B. {url}",
"DownloadClientSettingsUseSslHelpText": "Sichere Verbindung verwenden, wenn Verbindung zu {clientName} hergestellt wird",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads; leer lassen, um den Standardspeicherort für Übertragungen zu verwenden",
"CustomFilter": "Benutzerdefinierter Filter",
"DownloadClientSettingsInitialState": "Ausgangszustand",
"Donate": "Spenden",
"DownloadClientFreeboxSettingsAppId": "App-ID",
"DownloadClientFreeboxSettingsAppIdHelpText": "App-ID, die beim Erstellen des Zugriffs auf die Freebox-API angegeben wird (z. B. „app_id“)",
"DownloadClientFreeboxSettingsPortHelpText": "Port, der für den Zugriff auf die Freebox-Schnittstelle verwendet wird, standardmäßig ist „{port}“",
"DownloadClientNzbgetSettingsAddPausedHelpText": "Diese Option erfordert mindestens NzbGet Version 16.0",
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Laden Sie zuerst das erste und das letzte Stück herunter (qBittorrent 4.1.0+)",
"IndexerBeyondHDSettingsSearchTypes": "Suchtyp",
"DownloadClientFloodSettingsUrlBaseHelpText": "Fügt der Flood-API ein Präfix hinzu, z. B. {url}",
"DownloadClientFreeboxSettingsApiUrl": "API-URL",
"DownloadClientFreeboxSettingsApiUrlHelpText": "Definieren Sie die Freebox-API-Basis-URL mit der API-Version, z. B. „{url}“, standardmäßig ist „{defaultApiUrl}“.",
"DownloadClientRTorrentSettingsUrlPathHelpText": "Pfad zum XMLRPC-Endpunkt, siehe {url}. Dies ist normalerweise RPC2 oder [Pfad zu ruTorrent]{url2}, wenn ruTorrent verwendet wird.",
"DownloadClientSettingsAddPaused": "Pausiert hinzufügen",
"SelectDownloadClientModalTitle": "{modalTitle} Wähle Download-Client",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "Erster und Letzter Erster",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Ausgangszustand für zu qBittorrent hinzugefügte Torrents. Beachten Sie, dass erzwungene Torrents nicht den Seed-Beschränkungen unterliegen",
"IndexerSettingsAppsMinimumSeeders": "Apps Mindestanzahl von Seedern",
"IndexerHDBitsSettingsMediums": "Medium",
"Destination": "Ziel",
"Directory": "Verzeichnis",
"DownloadClientDelugeSettingsUrlBaseHelpText": "Fügt der Deluge-JSON-URL ein Präfix hinzu, siehe {url}",
"DownloadClientFloodSettingsAdditionalTags": "Zusätzliche Tags",
"DownloadClientFloodSettingsTagsHelpText": "Erste Tags eines Downloads. Um erkannt zu werden, muss ein Download über alle Anfangs-Tags verfügen. Dies vermeidet Konflikte mit nicht verwandten Downloads.",
"DownloadClientFreeboxSettingsAppToken": "App-Token",
"DownloadClientFreeboxSettingsAppTokenHelpText": "App-Token, das beim Erstellen des Zugriffs auf die Freebox-API abgerufen wird (z. B. „app_token“)",
"DownloadClientFreeboxSettingsHostHelpText": "Hostname oder Host-IP-Adresse der Freebox, standardmäßig „{url}“ (funktioniert nur im selben Netzwerk)",
"DownloadClientPneumaticSettingsNzbFolder": "NZB-Ordner",
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Dieser Ordner muss über XBMC erreichbar sein",
"DownloadClientPneumaticSettingsStrmFolder": "Strm-Ordner",
"DownloadClientPneumaticSettingsStrmFolderHelpText": ".strm-Dateien in diesem Ordner werden von der Drohne importiert",
"DownloadClientQbittorrentSettingsSequentialOrder": "Fortlaufende Reihenfolge",
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "In sequentieller Reihenfolge herunterladen (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentSettingsUseSslHelpText": "Verwenden Sie eine sichere Verbindung. Siehe Optionen -> Web-Benutzeroberfläche -> „HTTPS statt HTTP verwenden“ in qBittorrent.",
"DownloadClientRTorrentSettingsAddStopped": "Hinzufügen gestoppt",
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Durch die Aktivierung werden Torrents und Magnete im gestoppten Zustand zu rTorrent hinzugefügt. Dadurch können Magnetdateien beschädigt werden.",
"DownloadClientRTorrentSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads. Lassen Sie das Feld leer, um den standardmäßigen rTorrent-Speicherort zu verwenden",
"DownloadClientRTorrentSettingsUrlPath": "URL-Pfad",
"IndexerHDBitsSettingsCodecs": "Codec",
"IndexerSettingsVipExpiration": "VIP Ablaufdatum",
"TorrentBlackholeSaveMagnetFilesHelpText": "Speichern Sie den Magnet-Link, wenn keine .torrent-Datei verfügbar ist (nur nützlich, wenn der Download-Client in einer Datei gespeicherte Magnete unterstützt)",
"TorrentBlackholeTorrentFolder": "Torrent-Ordner",
"UseSsl": "SSL verwenden",
"UsenetBlackholeNzbFolder": "NZB-Ordner",
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Für Magnet-Links zu verwendende Erweiterung, standardmäßig „.magnet“.",
"XmlRpcPath": "XML-RPC-Pfad",
"ProxyValidationBadRequest": "Proxy konnte nicht getestet werden. StatusCode: {statusCode}",
"SecretToken": "Geheimer Token",
"DownloadClientDownloadStationSettingsDirectoryHelpText": "Optionaler freigegebener Ordner zum Ablegen von Downloads. Lassen Sie das Feld leer, um den Standardspeicherort der Download Station zu verwenden",
"DownloadClientFloodSettingsAdditionalTagsHelpText": "Fügt Eigenschaften von Medien als Tags hinzu. Hinweise sind Beispiele.",
"TorrentBlackholeSaveMagnetFiles": "Speicher Magnetdateien",
"TorrentBlackholeSaveMagnetFilesExtension": "Speicher die Magnet-Dateienerweiterung",
"Default": "Standard",
"GrabRelease": "Release erfassen"
}

View File

@@ -431,7 +431,7 @@
"IndexerInfo": "Πληροφορίες ευρετηρίου",
"IndexerName": "Όνομα ευρετηρίου",
"IndexerProxies": "Proxer Indexer",
"IndexerNoDefCheckMessage": "Τα ευρετήρια δεν έχουν ορισμό και δεν θα λειτουργήσουν: {0}. Αφαιρέστε και (ή) προσθέστε ξανά στο {appName}",
"IndexerNoDefinitionCheckHealthCheckMessage": "Τα ευρετήρια δεν έχουν ορισμό και δεν θα λειτουργήσουν: {0}. Αφαιρέστε και (ή) προσθέστε ξανά στο {appName}",
"SemiPrivate": "Ημι-ιδιωτικό",
"SettingsIndexerLoggingHelpText": "Καταγραφή πρόσθετων δεδομένων ευρετηρίου συμπεριλαμβανομένης της απόκρισης",
"SearchTypes": "Τύποι αναζήτησης",
@@ -522,5 +522,7 @@
"IndexerBeyondHDSettingsSearchTypes": "Τύποι αναζήτησης",
"IndexerHDBitsSettingsMediums": "Μεσαίο",
"UseSsl": "Χρησιμοποιήστε SSL",
"CustomFilter": "Custom Φιλτρα"
"CustomFilter": "Custom Φιλτρα",
"GrabRelease": "Πιάσε την απελευθέρωση",
"ProxyValidationBadRequest": "Αποτυχία δοκιμής διακομιστή μεσολάβησης. StatusCode: {statusCode}"
}

View File

@@ -143,6 +143,7 @@
"DatabaseMigration": "Database Migration",
"Date": "Date",
"Dates": "Dates",
"Default": "Default",
"DefaultCategory": "Default Category",
"DefaultNameCopiedProfile": "{name} - Copy",
"Delete": "Delete",
@@ -286,6 +287,7 @@
"GeneralSettingsSummary": "Port, SSL, username/password, proxy, analytics, and updates",
"Genre": "Genre",
"GoToApplication": "Go to application",
"GrabRelease": "Grab Release",
"GrabReleases": "Grab Release(s)",
"GrabTitle": "Grab Title",
"Grabbed": "Grabbed",
@@ -366,7 +368,7 @@
"IndexerNewznabSettingsAdditionalParametersHelpText": "Additional Newznab parameters",
"IndexerNewznabSettingsApiKeyHelpText": "Site API Key",
"IndexerNewznabSettingsVipExpirationHelpText": "Enter date (yyyy-mm-dd) for VIP Expiration or blank, {appName} will notify 1 week from expiration of VIP",
"IndexerNoDefCheckMessage": "Indexers have no definition and will not work: {0}. Please remove and (or) re-add to {appName}",
"IndexerNoDefinitionCheckHealthCheckMessage": "Indexers have no definition and will not work: {indexerNames}. Please remove and (or) re-add to {appName}.",
"IndexerNzbIndexSettingsApiKeyHelpText": "Site API Key",
"IndexerObsoleteCheckMessage": "Indexers are obsolete or have been updated: {0}. Please remove and (or) re-add to {appName}",
"IndexerOrpheusSettingsApiKeyHelpText": "API Key from the Site (Found in Settings => Access Settings)",
@@ -448,6 +450,7 @@
"ManageClients": "Manage Clients",
"ManageDownloadClients": "Manage Download Clients",
"Manual": "Manual",
"ManualGrab": "Manual Grab",
"MappedCategories": "Mapped Categories",
"MappedDrivesRunningAsService": "Mapped network drives are not available when running as a Windows Service. Please see the FAQ for more information",
"MassEditor": "Mass Editor",
@@ -508,9 +511,12 @@
"OnHealthIssueHelpText": "On Health Issue",
"OnHealthRestored": "On Health Restored",
"OnHealthRestoredHelpText": "On Health Restored",
"Open": "Open",
"OpenBrowserOnStart": "Open browser on start",
"OpenThisModal": "Open This Modal",
"Options": "Options",
"OverrideAndAddToDownloadClient": "Override and add to download client",
"OverrideGrabModalTitle": "Override and Grab - {title}",
"PackSeedTime": "Pack Seed Time",
"PackSeedTimeHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default",
"PackageVersion": "Package Version",
@@ -527,6 +533,7 @@
"PortNumber": "Port Number",
"Presets": "Presets",
"Priority": "Priority",
"PrioritySettings": "Priority: {priority}",
"Privacy": "Privacy",
"Private": "Private",
"Protocol": "Protocol",
@@ -543,6 +550,8 @@
"ProxyResolveIpHealthCheckMessage": "Failed to resolve the IP Address for the Configured Proxy Host {proxyHostName}",
"ProxyType": "Proxy Type",
"ProxyUsernameHelpText": "You only need to enter a username and password if one is required. Leave them blank otherwise.",
"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",
"Publisher": "Publisher",
"Query": "Query",
@@ -610,6 +619,7 @@
"SeedTimeHelpText": "The time a torrent should be seeded before stopping, empty is app's default",
"Seeders": "Seeders",
"SelectAll": "Select All",
"SelectDownloadClientModalTitle": "{modalTitle} - Select Download Client",
"SelectIndexers": "Select Indexers",
"SelectedCountOfCountReleases": "Selected {selectedCount} of {itemCount} releases",
"SemiPrivate": "Semi-Private",

View File

@@ -29,10 +29,10 @@
"View": "Vista",
"Updates": "Actualizaciones",
"UpdateUiNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de interfaz de usuario '{uiFolder}' no es modificable por el usuario '{userName}'.",
"UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' está en una carpeta de \"App Translocation\".",
"UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' está en una carpeta de translocación de aplicaciones.",
"UpdateStartupNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' no tiene permisos de escritura para el usuario '{userName}'.",
"UnselectAll": "Desmarcar todo",
"UI": "UI",
"UI": "Interfaz",
"Tasks": "Tareas",
"Tags": "Etiquetas",
"System": "Sistema",
@@ -167,7 +167,7 @@
"URLBase": "URL base",
"Uptime": "Tiempo de actividad",
"UpdateScriptPathHelpText": "Ruta a un script personalizado que toma un paquete de actualización extraído y gestiona el resto del proceso de actualización",
"UpdateMechanismHelpText": "Usar el actualizador incorporado de {appName} o un script",
"UpdateMechanismHelpText": "Usar el actualizador integrado de {appName} o un script",
"UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Todavía puedes instalar desde Sistema: Actualizaciones",
"UnableToLoadTags": "No se pueden cargar las Etiquetas",
"UnableToLoadNotifications": "No se pueden cargar las Notificaciones",
@@ -214,7 +214,7 @@
"LogLevel": "Nivel de Registro",
"LaunchBrowserHelpText": " Abrir un navegador web e ir a la página de inicio de {appName} al arrancar la app.",
"Interval": "Intervalo",
"IndexerFlags": "Banderas del indexador",
"IndexerFlags": "Indicadores del indexador",
"IncludeHealthWarningsHelpText": "Incluir Alertas de Salud",
"IllRestartLater": "Lo reiniciaré más tarde",
"IgnoredAddresses": "Ignorar direcciones",
@@ -250,12 +250,12 @@
"UnableToLoadGeneralSettings": "No se han podido cargar los ajustes Generales",
"UnableToLoadBackups": "No se pudo cargar las copias de seguridad",
"UnableToAddANewNotificationPleaseTryAgain": "No se ha podido añadir una nueva notificación, prueba otra vez.",
"UnableToAddANewIndexerPleaseTryAgain": "No se ha podido añadir un nuevo indexer, prueba otra vez.",
"UnableToAddANewIndexerPleaseTryAgain": "No se pudo añadir un nuevo indexador, por favor inténtalo de nuevo.",
"UnableToAddANewDownloadClientPleaseTryAgain": "No se ha podido añadir un nuevo gestor de descargas, prueba otra vez.",
"TagIsNotUsedAndCanBeDeleted": "La etiqueta no se usa y puede ser borrada",
"StartTypingOrSelectAPathBelow": "Comienza a escribir o selecciona una ruta debajo",
"Restore": "Restaurar",
"ProwlarrSupportsAnyIndexer": "{appName} soporta cualquier indexer que utilice el estandar Newznab, como también cualquiera de los indexers listados debajo.",
"ProwlarrSupportsAnyIndexer": "{appName} soporta muchos indexadores junto a cualquier indexador que utilice el estándar Newznab/Torznab usando 'Newznab genérico' (para usenet) o 'Torznab genérico' (para torrents). Busca y selecciona tu indexador a continuación.",
"ProwlarrSupportsAnyDownloadClient": "{appName} soporta cualquier gestor de descargas indicado debajo.",
"NoUpdatesAreAvailable": "No hay actualizaciones disponibles",
"NoTagsHaveBeenAddedYet": "Ninguna etiqueta ha sido añadida aún",
@@ -271,9 +271,9 @@
"UILanguage": "Lenguaje de UI",
"Priority": "Prioridad",
"InteractiveSearch": "Búsqueda Interactiva",
"IndexerPriorityHelpText": "Prioridad del Indexer de 1 (La más alta) a 50 (La más baja). Por defecto: 25.",
"IndexerPriorityHelpText": "Prioridad del indexador de 1 (la más alta) a 50 (la más baja). Predeterminado: 25.",
"IndexerPriority": "Prioridad del indexador",
"EditIndexer": "Editar Indexer",
"EditIndexer": "Editar indexador",
"Disabled": "Deshabilitado",
"AutomaticSearch": "Búsqueda Automática",
"AddIndexer": "Añadir Indexador",
@@ -284,8 +284,8 @@
"MovieIndexScrollBottom": "Indice de Películas: Desplazar hacia abajo",
"CloseCurrentModal": "Cerrar esta Ventana Modal",
"AcceptConfirmationModal": "Aceptar Confirmación de esta Ventana Modal",
"IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexers no disponible por errores durando más de 6 horas: {indexerNames}",
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexer está disponible por errores durando más de 6 horas",
"IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores durante más de 6 horas: {indexerNames}",
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador está disponible debido a errores durante más de 6 horas",
"Reddit": "Reddit",
"UnableToAddANewAppProfilePleaseTryAgain": "No se ha podido añadir un nuevo perfil de calidad, prueba otra vez.",
"DeleteIndexerProxyMessageText": "¿Seguro que quieres eliminar el proxy indexador '{name}'?",
@@ -295,21 +295,21 @@
"Donations": "Donaciones",
"SearchIndexers": "Buscar Indexadores",
"Enabled": "Habilitado",
"Grabs": "Capturar",
"Grabs": "Capturas",
"Presets": "Preajustes",
"Rss": "RSS",
"Today": "Hoy",
"Tomorrow": "Mañana",
"Torrent": "Torrents",
"UnableToAddANewIndexerProxyPleaseTryAgain": "No se ha podido añadir un nuevo indexer, prueba otra vez.",
"UnableToAddANewIndexerProxyPleaseTryAgain": "No se pudo añadir un nuevo proxy de indexador, por favor inténtalo de nuevo.",
"Wiki": "Wiki",
"Yesterday": "Ayer",
"ApplicationStatusCheckAllClientMessage": "Las listas no están disponibles debido a errores",
"ApplicationStatusCheckSingleClientMessage": "Listas no disponibles debido a errores: {0}",
"AllIndexersHiddenDueToFilter": "Todos los indexadores están ocultas debido al filtro aplicado.",
"DeleteApplicationMessageText": "Seguro que quieres eliminar la notificación '{name}'?",
"IndexerProxyStatusAllUnavailableHealthCheckMessage": "Los indexers no están disponibles debido a errores",
"IndexerProxyStatusUnavailableHealthCheckMessage": "Indexers no disponibles debido a errores: {indexerProxyNames}",
"IndexerProxyStatusAllUnavailableHealthCheckMessage": "Ningún indexador disponible debido a errores",
"IndexerProxyStatusUnavailableHealthCheckMessage": "Proxies de indexador no disponibles debido a errores: {indexerProxyNames}",
"NoLinks": "Sin enlaces",
"AddDownloadClient": "Añadir Cliente de Descarga",
"CouldNotConnectSignalR": "No se pudo conectar a SignalR, la interfaz de usuario no se actualiza",
@@ -342,13 +342,13 @@
"Encoding": "Codificación",
"GrabReleases": "Capturar Lanzamiento(s)",
"Link": "Enlace",
"MappedDrivesRunningAsService": "Las unidades de red asignadas no están disponibles cuando se ejecutan como un servicio de Windows. Consulte las preguntas frecuentes para obtener más información",
"MappedDrivesRunningAsService": "Las unidades de red asignadas no están disponibles cuando se ejecutan como un servicio de Windows. Consulta las preguntas frecuentes para obtener más información",
"No": "No",
"Notification": "Notificaciones",
"Notification": "Notificación",
"Notifications": "Notificaciones",
"UnableToLoadIndexers": "No se pueden cargar los indexadores",
"Yes": "Sí",
"UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent proporcionado por la aplicación llamó a la API",
"UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent proporcionado por la aplicación que llamó a la API",
"InstanceName": "Nombre de la Instancia",
"InstanceNameHelpText": "Nombre de la instancia en la pestaña y para la aplicación Syslog",
"Database": "Base de datos",
@@ -374,9 +374,9 @@
"Theme": "Tema",
"ApplyTagsHelpTextAdd": "Añadir: Añade las etiquetas a la lista de etiquetas existente",
"DeleteSelectedApplicationsMessageText": "¿Está seguro de querer eliminar {count} aplicación(es) de seleccionada(s)?",
"DeleteSelectedDownloadClients": "Borrar Cliente de Descarga(s)",
"DeleteSelectedIndexersMessageText": "¿Está seguro de querer eliminar {count} indexador(es) seleccionado(s)?",
"DeleteSelectedDownloadClientsMessageText": "¿Está seguro de querer eliminar {count} cliente(s) de descarga seleccionado(s)?",
"DeleteSelectedDownloadClients": "Borrar cliente(s) de descarga",
"DeleteSelectedIndexersMessageText": "¿Estás seguro que quieres eliminar {count} indexador(es) seleccionado(s)?",
"DeleteSelectedDownloadClientsMessageText": "¿Estás seguro que quieres eliminar {count} cliente(s) de descarga seleccionado(s)?",
"ApplyTagsHelpTextHowToApplyApplications": "Cómo aplicar etiquetas a las aplicaciones seleccionadas",
"SelectIndexers": "Seleccionar Indexadores",
"ApplyTagsHelpTextHowToApplyIndexers": "Cómo aplicar etiquetas a los indexadores seleccionados",
@@ -386,7 +386,7 @@
"DownloadClientPriorityHelpText": "Priorizar varios Clientes de Descarga. Round-Robin se utiliza para clientes con la misma prioridad.",
"Season": "Temporada",
"More": "Más",
"Track": "Rastro",
"Track": "Pista",
"Year": "Año",
"UpdateAvailableHealthCheckMessage": "La nueva actualización está disponible",
"Genre": "Género",
@@ -402,17 +402,17 @@
"ApiKeyValidationHealthCheckMessage": "Actualice su clave de API para que tenga al menos {length} carácteres. Puede hacerlo en los ajustes o en el archivo de configuración",
"IndexerDownloadClientHealthCheckMessage": "Indexadores con clientes de descarga inválidos: {indexerNames}.",
"Episode": "Episodio",
"ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puede hacer clic en recargar abajo.",
"ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para recuperar su funcionalidad.",
"ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puedes pulsar en recargar abajo.",
"ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para restaurar su funcionalidad.",
"RecentChanges": "Cambios recientes",
"WhatsNew": "Que es lo nuevo?",
"minutes": "Minutos",
"WhatsNew": "¿Qué hay nuevo?",
"minutes": "minutos",
"Album": "Álbum",
"Artist": "Artista",
"DeleteAppProfileMessageText": "¿Estás seguro de que quieres eliminar el perfil de la aplicación '{name}'?",
"AddConnection": "Añadir Conexión",
"NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a fallos",
"NotificationStatusSingleClientHealthCheckMessage": "Listas no disponibles debido a errores: {notificationNames}",
"NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a errores",
"NotificationStatusSingleClientHealthCheckMessage": "Notificaciones no disponibles debido a errores: {notificationNames}",
"EditIndexerImplementation": "Editar Indexador - {implementationName}",
"AuthBasic": "Básico (ventana emergente del navegador)",
"AuthForm": "Formularios (Página de inicio de sesión)",
@@ -496,7 +496,7 @@
"InvalidUILanguage": "Su interfaz de usuario está configurada en un idioma no válido, corríjalo y guarde la configuración",
"DownloadClientQbittorrentSettingsContentLayout": "Diseño del contenido",
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si usar el diseño de contenido configurado de qBittorrent, el diseño original del torrent o siempre crear una subcarpeta (qBittorrent 4.3.2+)",
"EnableRssHelpText": "Habilitar feed RSS para el Indexador",
"EnableRssHelpText": "Habilitar canal RSS para el Indexador",
"days": "días",
"ElapsedTime": "Tiempo transcurrido",
"GrabTitle": "Capturar título",
@@ -505,29 +505,29 @@
"Redirect": "Redirección",
"RssQueries": "Consultas RSS",
"SeedRatio": "Proporción de Semillado",
"RssFeed": "Feed RSS",
"RssFeed": "Canal RSS",
"SearchType": "Tipo de búsqueda",
"RepeatSearch": "Repetir búsqueda",
"SeedRatioHelpText": "El ratio que un torrent debe alcanzar antes de detenerse, si está vacío se usará el valor por defecto",
"SeedTime": "Tiempo de Semillado",
"SearchTypes": "Tipos de búsquedas",
"DeleteIndexerProxy": "Eliminar proxy indexador",
"OnGrabHelpText": "Al Capturar lanzamiento",
"SeedTimeHelpText": "El ratio que un torrent debe alcanzar antes de detenerse, si está vacío se usa el valor por defecto",
"OnGrabHelpText": "Al capturar lanzamiento",
"SeedTimeHelpText": "El tiempo que un torrent debería ser sembrado antes de detenerse, si está vacío usa el valor predeterminado",
"IndexerTagsHelpTextWarning": "Las etiquetas deben utilizarse con precaución, ya que pueden tener efectos no deseados. Un indexador con una etiqueta solo se sincronizará con aplicaciones que tengan la misma etiqueta.",
"TVSearchTypes": "Tipos de búsquedas",
"TVSearchTypes": "Tipos de búsqueda de TV",
"DownloadClientAriaSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación de Aria2 predeterminada",
"IndexerNoDefCheckMessage": "Los indexadores no tienen definición y no funcionarán: {0}. Por favor elimínelos y (o) vuelva a añadirlos a {appName}",
"IndexerNoDefinitionCheckHealthCheckMessage": "Los indexadores no tienen definición y no funcionarán: {0}. Por favor elimínelos y (o) vuelva a añadirlos a {appName}",
"IndexerProxy": "Proxy del Indexador",
"IndexerObsoleteCheckMessage": "Los indexadores están obsoletos o se han actualizado: {0}. Por favor elimínelos y (o) vuelva a añadirlos a {appName}",
"IncludeManualGrabsHelpText": "Incluir las Capturas Manuales realizadas en {appName}",
"IndexerRss": "RSS del Indexador",
"IndexerSettingsSummary": "Configurar varios ajustes globales del Indexador, incluyendo Proxies.",
"IndexerName": "Nombre del Indexador",
"IndexerVipExpiringHealthCheckMessage": "Las ventajas VIP de los indexadores expiran pronto: {indexerNames}",
"IndexerVipExpiringHealthCheckMessage": "Las ventajas VIP del indexador expiran pronto: {indexerNames}",
"IndexerProxies": "Proxies del Indexador",
"IndexerHistoryLoadError": "Error al cargar el historial del Indexador",
"IndexerVipExpiredHealthCheckMessage": "Las ventajas VIP del Indexador han caducado: {indexerNames}",
"IndexerVipExpiredHealthCheckMessage": "Las ventajas VIP del indexador han expirado: {indexerNames}",
"IndexerQuery": "Consulta dedl Indexador",
"IndexerSite": "Sitio del Indexador",
"IndexerStatus": "Estado del indexador",
@@ -613,7 +613,7 @@
"IndexerHDBitsSettingsPasskeyHelpText": "Clave de acceso desde los Detalles de Usuario",
"IndexerSettingsPasskey": "Clave de Acceso",
"BlackholeFolderHelpText": "La carpeta en donde {appName} se almacenaran los {extension} file",
"CustomFilter": "Filtros personalizados",
"CustomFilter": "Filtro personalizado",
"LabelIsRequired": "Se requiere etiqueta",
"TorrentBlackholeSaveMagnetFiles": "Guardar archivos magnet",
"TorrentBlackholeTorrentFolder": "Carpeta de torrent",
@@ -645,7 +645,7 @@
"DownloadClientRTorrentSettingsUrlPath": "Ruta de url",
"IndexerHDBitsSettingsOrigins": "Orígenes",
"IndexerIPTorrentsSettingsCookieUserAgentHelpText": "User-Agent asociado con la cookie usada desde el navegador",
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, vacío usa el predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de parar, vacío usa el predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Sincronizar rechazo de hashes de torrents en la lista de bloqueo mientras se captura",
"IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Debes tener permisos de usuario y de torrents",
"IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Busca lanzamientos por nombres de grupo",
@@ -679,7 +679,7 @@
"IndexerSettingsCookie": "Cookie",
"IndexerSettingsRssKey": "Clave RSS",
"IndexerSettingsSeedRatio": "Ratio de sembrado",
"IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser compartido antes de detenerse, vac usa el predeterminado del cliente de descarga",
"IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser sembrado antes de parar, vacío usa el predeterminado del cliente de descarga",
"IndexerMTeamTpSettingsApiKeyHelpText": "Clave API del sitio (encontrada en Panel de control de usuario => Seguridad => Laboratorio)",
"IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech",
"Menu": "Menú",
@@ -702,7 +702,7 @@
"IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech",
"IndexerBeyondHDSettingsLimitedOnlyHelpText": "Buscar solo freeleech (UL limitada)",
"IndexerFileListSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech",
"IndexerFileListSettingsPasskeyHelpText": "Clave de acceso del sitio (esto es la cadena alfanumérica en la url del tracket mostrada en tu cliente de descarga)",
"IndexerFileListSettingsPasskeyHelpText": "Clave de acceso del sitio (Esto es la cadena alfanumérica en la url del tracker mostrada en tu cliente de descarga)",
"IndexerFileListSettingsUsernameHelpText": "Usuario del sitio",
"IndexerHDBitsSettingsMediums": "Medios",
"IndexerHDBitsSettingsCodecs": "Códecs",
@@ -749,5 +749,16 @@
"IndexerSettingsVipExpiration": "Expiración del VIP",
"XmlRpcPath": "Ruta RPC de XML",
"NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} en el título",
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente prefija el título del mensaje con {appName} para diferenciar las notificaciones de las diferentes aplicaciones"
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente prefija el título del mensaje con {appName} para diferenciar las notificaciones de las diferentes aplicaciones",
"IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Busca solo lanzamientos freeleech",
"ProxyValidationBadRequest": "Fallo al probar el proxy. Código de estado: {statusCode}",
"ProxyValidationUnableToConnect": "No se pudo conectar al proxy: {exceptionMessage}. Comprueba el registro de este error para más detalles",
"GrabRelease": "Capturar lanzamiento",
"ManualGrab": "Captura manual",
"Open": "Abrir",
"OverrideAndAddToDownloadClient": "Sobrescribir y añadir al cliente de descarga",
"OverrideGrabModalTitle": "Sobrescribir y capturar - {title}",
"PrioritySettings": "Prioridad: {priority}",
"SelectDownloadClientModalTitle": "{modalTitle} - Seleccionar cliente de descarga",
"Default": "Predeterminado"
}

View File

@@ -405,7 +405,7 @@
"ApplicationsLoadError": "Sovelluslistausta ei voitu ladata",
"Url": "URL",
"Website": "Verkkosivusto",
"IndexerNoDefCheckMessage": "Tietolähteillä ei ole määritystä, eivätkä ne toimi: {0}. Poista ja/tai lisää {appName}iin uudelleen",
"IndexerNoDefinitionCheckHealthCheckMessage": "Tietolähteillä ei ole määritystä, eivätkä ne toimi: {0}. Poista ja/tai lisää {appName}iin uudelleen",
"Private": "Yksityinen",
"QueryResults": "Kyselyn tulokset",
"Application": "Sovellus",
@@ -668,7 +668,7 @@
"DownloadClientFreeboxSettingsPortHelpText": "Freebox-liittymän portti. Oletus on \"{port}\".",
"DownloadClientPneumaticSettingsNzbFolder": "NZB-kansio",
"DownloadClientQbittorrentSettingsSequentialOrder": "Peräkkäinen järjestys",
"CustomFilter": "Omat suodattimet",
"CustomFilter": "Oma suodatin",
"DownloadClientFreeboxSettingsAppIdHelpText": "Freebox-rajapinnan käyttöoikeutta määritettäessä käytettävä App ID -sovellustunniste.",
"DownloadClientFreeboxSettingsAppToken": "Sovellustietue",
"DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä vaatii vähintään NzbGet-version 16.0.",
@@ -680,5 +680,15 @@
"TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain lataustyökalun tukiessa tiedostoon tallennettuja magnet-linkkejä).",
"TorrentBlackholeTorrentFolder": "Torrent-kansio",
"TorrentBlackholeSaveMagnetFilesExtension": "Tallennettujen magnet-tiedostojen pääte",
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet-linkeille käytettävä tiedostopääte. Oletus on \".magnet\"."
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet-linkeille käytettävä tiedostopääte. Oletus on \".magnet\".",
"LabelIsRequired": "Nimi on pakollinen",
"Default": "Oletus",
"GrabRelease": "Kaappaa julkaisu",
"OverrideGrabModalTitle": "Ohita ja sieppaa - {title}",
"PrioritySettings": "Painotus: {priority}",
"SelectDownloadClientModalTitle": "{modalTitle} - Valitse lataustyökalu",
"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"
}

Some files were not shown because too many files have changed in this diff Show More