diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e2c4dd98d69..c7b8da73ed2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,7 +9,7 @@ variables: testsFolder: './_tests' yarnCacheFolder: $(Pipeline.Workspace)/.yarn nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages - majorVersion: '2.5.2' + majorVersion: '2.6.5' minorVersion: $[counter('minorVersion', 1)] prowlarrVersion: '$(majorVersion).$(minorVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' diff --git a/frontend/src/Components/Form/NewznabCategorySelectInputConnector.js b/frontend/src/Components/Form/NewznabCategorySelectInputConnector.js index e63e2cae773..fa60d4386d3 100644 --- a/frontend/src/Components/Form/NewznabCategorySelectInputConnector.js +++ b/frontend/src/Components/Form/NewznabCategorySelectInputConnector.js @@ -8,7 +8,9 @@ function createMapStateToProps() { return createSelector( (state, { value }) => value, (state) => state.settings.indexerCategories, - (value, categories) => { + (state) => state.indexers.items, + (state, { indexerIds }) => indexerIds, + (value, categories, indexers, indexerIds) => { const values = []; categories.items.forEach((element) => { @@ -30,6 +32,31 @@ function createMapStateToProps() { } }); + if (indexerIds && indexerIds.length === 1) { + const indexer = indexers.find((i) => i.id === indexerIds[0]); + const indexerCategories = (indexer?.capabilities?.categories ?? []) + .filter((category) => category.id >= 100000); + + if (indexerCategories.length) { + const customCategoryGroup = { + key: `indexerCategories-${indexer.id}`, + value: indexer.name, + isDisabled: true + }; + + values.push(customCategoryGroup); + + indexerCategories.forEach((category) => { + values.push({ + key: category.id, + value: category.name, + hint: `(${category.id})`, + parentKey: customCategoryGroup.key + }); + }); + } + } + return { value: value || [], values @@ -60,7 +87,7 @@ class IndexersSelectInputConnector extends Component { IndexersSelectInputConnector.propTypes = { name: PropTypes.string.isRequired, - indexerIds: PropTypes.number, + indexerIds: PropTypes.arrayOf(PropTypes.number), value: PropTypes.arrayOf(PropTypes.number).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired, onChange: PropTypes.func.isRequired diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx index 6b7da03eb77..a3c99ac4e40 100644 --- a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx @@ -21,7 +21,7 @@ function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) { const dispatch = useDispatch(); - const { authentication, isDocker } = useSelector( + const { authentication, isContainerized } = useSelector( (state: AppState) => state.system.status.item ); @@ -48,24 +48,22 @@ function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) { {translate('KeyboardShortcuts')} - {isDocker ? null : ( - <> - + - - - {translate('Restart')} - + + + {translate('Restart')} + - - - {translate('Shutdown')} - - + {isContainerized ? null : ( + + + {translate('Shutdown')} + )} {formsAuth ? ( diff --git a/frontend/src/FirstRun/AuthenticationRequiredModal.js b/frontend/src/FirstRun/AuthenticationRequiredModal.js deleted file mode 100644 index caa855cb795..00000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModal.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector'; - -function onModalClose() { - // No-op -} - -function AuthenticationRequiredModal(props) { - const { - isOpen - } = props; - - return ( - - - - ); -} - -AuthenticationRequiredModal.propTypes = { - isOpen: PropTypes.bool.isRequired -}; - -export default AuthenticationRequiredModal; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModal.tsx b/frontend/src/FirstRun/AuthenticationRequiredModal.tsx new file mode 100644 index 00000000000..1b4b519ce7a --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModal.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent'; + +function onModalClose() { + // No-op +} + +interface AuthenticationRequiredModalProps { + isOpen: boolean; +} + +export default function AuthenticationRequiredModal({ + isOpen, +}: AuthenticationRequiredModalProps) { + return ( + + + + ); +} diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js deleted file mode 100644 index 17a04e40310..00000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js +++ /dev/null @@ -1,170 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useEffect, useRef } from 'react'; -import Alert from 'Components/Alert'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -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 { inputTypes, kinds } from 'Helpers/Props'; -import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings'; -import translate from 'Utilities/String/translate'; -import styles from './AuthenticationRequiredModalContent.css'; - -function onModalClose() { - // No-op -} - -function AuthenticationRequiredModalContent(props) { - const { - isPopulated, - error, - isSaving, - settings, - onInputChange, - onSavePress, - dispatchFetchStatus - } = props; - - const { - authenticationMethod, - authenticationRequired, - username, - password, - passwordConfirmation - } = settings; - - const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; - - const didMount = useRef(false); - - useEffect(() => { - if (!isSaving && didMount.current) { - dispatchFetchStatus(); - } - - didMount.current = true; - }, [isSaving, dispatchFetchStatus]); - - return ( - - - {translate('AuthenticationRequired')} - - - - - {translate('AuthenticationRequiredWarning')} - - - { - isPopulated && !error ? -
- - {translate('AuthenticationMethod')} - - - - - - {translate('AuthenticationRequired')} - - - - - - {translate('Username')} - - - - - - {translate('Password')} - - - - - - {translate('PasswordConfirmation')} - - - -
: - null - } - - { - !isPopulated && !error ? : null - } -
- - - - {translate('Save')} - - -
- ); -} - -AuthenticationRequiredModalContent.propTypes = { - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - settings: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - dispatchFetchStatus: PropTypes.func.isRequired -}; - -export default AuthenticationRequiredModalContent; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx b/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx new file mode 100644 index 00000000000..99d4a2d57f2 --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx @@ -0,0 +1,211 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +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 usePrevious from 'Helpers/Hooks/usePrevious'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { + authenticationMethodOptions, + authenticationRequiredOptions, +} from 'Settings/General/SecuritySettings'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { + fetchGeneralSettings, + saveGeneralSettings, + setGeneralSettingsValue, +} from 'Store/Actions/settingsActions'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import styles from './AuthenticationRequiredModalContent.css'; + +const SECTION = 'general'; + +const selector = createSettingsSectionSelector(SECTION); + +function onModalClose() { + // No-op +} + +export default function AuthenticationRequiredModalContent() { + const { isPopulated, error, isSaving, saveError, settings } = + useSelector(selector); + const dispatch = useDispatch(); + + const { + authenticationMethod, + authenticationRequired, + username, + password, + passwordConfirmation, + allowedHosts, + } = settings; + + const wasSaving = usePrevious(isSaving); + + useEffect(() => { + dispatch(fetchGeneralSettings()); + + return () => { + dispatch(clearPendingChanges({ section: `settings.${SECTION}` })); + }; + }, [dispatch]); + + const onInputChange = useCallback( + (args: InputChanged) => { + // @ts-expect-error Actions aren't typed + dispatch(setGeneralSettingsValue(args)); + }, + [dispatch] + ); + + const authenticationEnabled = + authenticationMethod && authenticationMethod.value !== 'none'; + + useEffect(() => { + if (isSaving || !wasSaving || saveError) { + return; + } + + dispatch(fetchStatus()); + }, [isSaving, wasSaving, saveError, dispatch]); + + const onPress = useCallback(() => { + dispatch(saveGeneralSettings()); + }, [dispatch]); + + return ( + + {translate('AuthenticationRequired')} + + + + {translate('AuthenticationRequiredWarning')} + + + {isPopulated && !error ? ( +
+ + {translate('AuthenticationMethod')} + + + + + + {translate('AuthenticationRequired')} + + + + + + {translate('Username')} + + + + + + {translate('Password')} + + + + + + {translate('PasswordConfirmation')} + + + + + + {translate('AllowedHosts')} + + + +
+ ) : null} + + {!isPopulated && !error ? : null} +
+ + + + {translate('Save')} + + +
+ ); +} diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js b/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js deleted file mode 100644 index 6653a9d34dc..00000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js +++ /dev/null @@ -1,86 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions'; -import { fetchStatus } from 'Store/Actions/systemActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; -import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent'; - -const SECTION = 'general'; - -function createMapStateToProps() { - return createSelector( - createSettingsSectionSelector(SECTION), - (sectionSettings) => { - return { - ...sectionSettings - }; - } - ); -} - -const mapDispatchToProps = { - dispatchClearPendingChanges: clearPendingChanges, - dispatchSetGeneralSettingsValue: setGeneralSettingsValue, - dispatchSaveGeneralSettings: saveGeneralSettings, - dispatchFetchGeneralSettings: fetchGeneralSettings, - dispatchFetchStatus: fetchStatus -}; - -class AuthenticationRequiredModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchGeneralSettings(); - } - - componentWillUnmount() { - this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` }); - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.dispatchSetGeneralSettingsValue({ name, value }); - }; - - onSavePress = () => { - this.props.dispatchSaveGeneralSettings(); - }; - - // - // Render - - render() { - const { - dispatchClearPendingChanges, - dispatchFetchGeneralSettings, - dispatchSetGeneralSettingsValue, - dispatchSaveGeneralSettings, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -AuthenticationRequiredModalContentConnector.propTypes = { - dispatchClearPendingChanges: PropTypes.func.isRequired, - dispatchFetchGeneralSettings: PropTypes.func.isRequired, - dispatchSetGeneralSettingsValue: PropTypes.func.isRequired, - dispatchSaveGeneralSettings: PropTypes.func.isRequired, - dispatchFetchStatus: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector); diff --git a/frontend/src/Search/SearchFooter.js b/frontend/src/Search/SearchFooter.js index 87232844625..0d522223f05 100644 --- a/frontend/src/Search/SearchFooter.js +++ b/frontend/src/Search/SearchFooter.js @@ -250,6 +250,7 @@ class SearchFooter extends Component { { + if (name === 'searchIndexerIds') { + const searchCategories = this.props.defaultCategories.filter( + (category) => category < 100000 + ); + + this.props.setSearchDefault({ + [name]: value, + searchCategories + }); + + return; + } + this.props.setSearchDefault({ [name]: value }); }; @@ -98,6 +111,7 @@ class SearchFooterConnector extends Component { SearchFooterConnector.propTypes = { defaultSearchQueryParams: PropTypes.object.isRequired, + defaultCategories: PropTypes.arrayOf(PropTypes.number).isRequired, setSearchDefault: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/General/GeneralSettings.js b/frontend/src/Settings/General/GeneralSettings.js index 7289ed1c7ce..dd05de25af8 100644 --- a/frontend/src/Settings/General/GeneralSettings.js +++ b/frontend/src/Settings/General/GeneralSettings.js @@ -22,6 +22,8 @@ const requiresRestartKeys = [ 'bindAddress', 'port', 'urlBase', + 'allowedHosts', + 'trustedNetworks', 'enableSsl', 'sslPort', 'sslCertPath', diff --git a/frontend/src/Settings/General/HostSettings.js b/frontend/src/Settings/General/HostSettings.js index 99255b0d556..64253ddd8f7 100644 --- a/frontend/src/Settings/General/HostSettings.js +++ b/frontend/src/Settings/General/HostSettings.js @@ -22,6 +22,7 @@ function HostSettings(props) { urlBase, instanceName, applicationUrl, + allowedHosts, enableSsl, sslPort, sslCertPath, @@ -74,6 +75,19 @@ function HostSettings(props) { /> + + {translate('AllowedHosts')} + + + + + {translate('TrustedNetworks')} + + + state.settings[section], - (sectionSettings) => { - const { - isFetching, - isPopulated, - error, - item, - pendingChanges, - isSaving, - saveError - } = sectionSettings; - - const settings = selectSettings(item, pendingChanges, saveError); - - return { - isFetching, - isPopulated, - error, - isSaving, - saveError, - ...settings - }; - } - ); -} - -export default createSettingsSectionSelector; diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.ts b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts new file mode 100644 index 00000000000..91983edc218 --- /dev/null +++ b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts @@ -0,0 +1,52 @@ +import { createSelector } from 'reselect'; +import { AppSectionItemState } from 'App/State/AppSectionState'; +import AppState from 'App/State/AppState'; +import SettingsAppState from 'App/State/SettingsAppState'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { PendingSection } from 'typings/pending'; + +type SectionsWithItemNames = { + [K in keyof SettingsAppState]: SettingsAppState[K] extends AppSectionItemState + ? K + : never; +}[keyof SettingsAppState]; + +type GetSectionState = + SettingsAppState[Name]; +type GetSettingsSectionItemType = + GetSectionState extends AppSectionItemState ? R : never; + +function createSettingsSectionSelector< + Name extends SectionsWithItemNames, + T extends GetSettingsSectionItemType +>(section: Name) { + return createSelector( + (state: AppState) => state.settings[section], + (sectionSettings) => { + const { item, pendingChanges, ...other } = sectionSettings; + + const saveError = + 'saveError' in sectionSettings ? sectionSettings.saveError : undefined; + + const isSaving = + 'isSaving' in sectionSettings ? sectionSettings.isSaving : false; + + const { + settings, + pendingChanges: selectedPendingChanges, + ...rest + } = selectSettings(item, pendingChanges, saveError); + + return { + ...other, + isSaving, + saveError, + settings: settings as PendingSection, + pendingChanges: selectedPendingChanges as Partial, + ...rest, + }; + } + ); +} + +export default createSettingsSectionSelector; diff --git a/frontend/src/System/Status/About/About.tsx b/frontend/src/System/Status/About/About.tsx index 6eb7fe9c747..807fbbdf89a 100644 --- a/frontend/src/System/Status/About/About.tsx +++ b/frontend/src/System/Status/About/About.tsx @@ -20,7 +20,7 @@ function About() { packageVersion, packageAuthor, isNetCore, - isDocker, + isContainerized, runtimeVersion, databaseVersion, databaseType, @@ -65,7 +65,7 @@ function About() { /> ) : null} - {isDocker ? ( + {isContainerized ? ( ) : null} diff --git a/frontend/src/typings/Settings/General.ts b/frontend/src/typings/Settings/General.ts index c867bed74ff..374b583340a 100644 --- a/frontend/src/typings/Settings/General.ts +++ b/frontend/src/typings/Settings/General.ts @@ -13,6 +13,7 @@ export default interface General { launchBrowser: boolean; authenticationMethod: string; authenticationRequired: string; + allowedHosts: string; analyticsEnabled: boolean; username: string; password: string; @@ -24,6 +25,7 @@ export default interface General { sslCertPath: string; sslCertPassword: string; urlBase: string; + trustedNetworks: string; instanceName: string; applicationUrl: string; updateAutomatically: boolean; diff --git a/frontend/src/typings/SystemStatus.ts b/frontend/src/typings/SystemStatus.ts index d5eab3ca34f..83d2b5e4336 100644 --- a/frontend/src/typings/SystemStatus.ts +++ b/frontend/src/typings/SystemStatus.ts @@ -9,7 +9,7 @@ interface SystemStatus { instanceName: string; isAdmin: boolean; isDebug: boolean; - isDocker: boolean; + isContainerized: boolean; isLinux: boolean; isNetCore: boolean; isOsx: boolean; diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts index c0fda305c83..cf91149b671 100644 --- a/frontend/src/typings/inputs.ts +++ b/frontend/src/typings/inputs.ts @@ -1,4 +1,6 @@ -export type CheckInputChanged = { +export type InputChanged = { name: string; - value: boolean; + value: T; }; + +export type CheckInputChanged = InputChanged; diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts new file mode 100644 index 00000000000..5cdcbc003d9 --- /dev/null +++ b/frontend/src/typings/pending.ts @@ -0,0 +1,23 @@ +export interface ValidationFailure { + propertyName: string; + errorMessage: string; + severity: 'error' | 'warning'; +} + +export interface ValidationError extends ValidationFailure { + isWarning: false; +} + +export interface ValidationWarning extends ValidationFailure { + isWarning: true; +} + +export interface Pending { + value: T; + errors: ValidationError[]; + warnings: ValidationWarning[]; +} + +export type PendingSection = { + [K in keyof T]: Pending; +}; diff --git a/package.json b/package.json index b75167fd560..17e0176735a 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "@types/react-router-dom": "5.3.3", "@types/react-text-truncate": "0.19.0", "@types/react-window": "1.8.8", + "@types/redux-actions": "2.6.5", "@types/webpack-livereload-plugin": "2.3.6", "@typescript-eslint/eslint-plugin": "8.18.1", "@typescript-eslint/parser": "8.18.1", diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1f869bdee78..b5eb86b8129 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -109,10 +109,6 @@ - - true - true @@ -125,9 +121,9 @@ - - - + + + diff --git a/src/NzbDrone.Api.Test/Authentication/UiAuthorizationHandlerFixture.cs b/src/NzbDrone.Api.Test/Authentication/UiAuthorizationHandlerFixture.cs new file mode 100644 index 00000000000..b9bc81ed9c9 --- /dev/null +++ b/src/NzbDrone.Api.Test/Authentication/UiAuthorizationHandlerFixture.cs @@ -0,0 +1,100 @@ +using System.Net; +using System.Security.Claims; +using FluentAssertions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using NUnit.Framework; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Http.Authentication; +using NzbDrone.Test.Common; + +namespace NzbDrone.Api.Test.Authentication +{ + [TestFixture] + public class UiAuthorizationHandlerFixture : TestBase + { + [SetUp] + public void Setup() + { + Mocker.GetMock() + .SetupGet(c => c.AuthenticationRequired) + .Returns(AuthenticationRequiredType.DisabledForLocalAddresses); + } + + private static HttpContext GetHttpContext(string remoteIp, string unconsumedForwardedFor) + { + var httpContext = new DefaultHttpContext(); + + httpContext.Connection.RemoteIpAddress = IPAddress.Parse(remoteIp); + + if (unconsumedForwardedFor != null) + { + httpContext.Request.Headers[ForwardedHeadersDefaults.XForwardedForHeaderName] = unconsumedForwardedFor; + } + + return httpContext; + } + + private bool IsAuthorized(HttpContext httpContext) + { + var requirement = new BypassableDenyAnonymousAuthorizationRequirement(); + var context = new AuthorizationHandlerContext(new[] { requirement }, new ClaimsPrincipal(), httpContext); + + Subject.HandleAsync(context).GetAwaiter().GetResult(); + + return context.HasSucceeded; + } + + [TestCase("127.0.0.1")] + [TestCase("192.168.1.50")] + [TestCase("10.0.0.5")] + public void should_bypass_authentication_for_local_address_without_forwarded_headers(string address) + { + IsAuthorized(GetHttpContext(address, null)).Should().BeTrue(); + } + + [Test] + public void should_not_bypass_authentication_for_public_address_without_forwarded_headers() + { + IsAuthorized(GetHttpContext("203.0.113.66", null)).Should().BeFalse(); + } + + [Test] + public void should_bypass_authentication_when_trusted_proxy_resolved_a_local_client() + { + IsAuthorized(GetHttpContext("192.168.1.50", null)).Should().BeTrue(); + } + + [Test] + public void should_not_bypass_authentication_when_trusted_proxy_resolved_a_public_client() + { + IsAuthorized(GetHttpContext("203.0.113.66", null)).Should().BeFalse(); + } + + [TestCase("127.0.0.1")] + [TestCase("192.168.1.50")] + [TestCase("10.0.0.5")] + public void should_not_bypass_authentication_for_forwarded_headers_from_an_untrusted_peer(string forwardedFor) + { + IsAuthorized(GetHttpContext("10.0.0.99", forwardedFor)).Should().BeFalse(); + } + + [Test] + public void should_not_bypass_authentication_for_a_truncated_proxy_chain() + { + IsAuthorized(GetHttpContext("10.0.0.2", "203.0.113.9")).Should().BeFalse(); + } + + [Test] + public void should_not_bypass_authentication_when_authentication_is_required() + { + Mocker.GetMock() + .SetupGet(c => c.AuthenticationRequired) + .Returns(AuthenticationRequiredType.Enabled); + + IsAuthorized(GetHttpContext("127.0.0.1", null)).Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs index fac3e20e795..d8412600827 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs @@ -540,6 +540,21 @@ public void MoveFolder_should_rename_caseinsensitive_folder() source.FullName.GetActualCasing().Should().Be(destination.FullName); } + [Test] + public void TransferFile_should_find_files_with_multiple_slashes_within_their_path() + { + WithRealDiskProvider(); + + var root = GetFilledTempFolder(); + var rootDir = root.FullName; + var from = Path.Combine(rootDir, "source-file"); + var toRootDir = rootDir.Replace(Path.DirectorySeparatorChar.ToString(), new string(Path.DirectorySeparatorChar, 3)); + var to = Path.Combine(toRootDir, "destination-file"); + File.WriteAllText(from, "Source file"); + var mode = Subject.TransferFile(from, to, TransferMode.Copy); + mode.Should().Be(TransferMode.Copy); + } + [Test] public void should_throw_if_destination_is_readonly() { @@ -927,7 +942,7 @@ private void VerifyCopyFolder(string source, string destination) var sourceFiles = Directory.GetFileSystemEntries(source, "*", SearchOption.AllDirectories).Select(v => v.Substring(source.Length + 1)).ToArray(); var destFiles = Directory.GetFileSystemEntries(destination, "*", SearchOption.AllDirectories).Select(v => v.Substring(destination.Length + 1)).ToArray(); - CollectionAssert.AreEquivalent(sourceFiles, destFiles); + Assert.That(sourceFiles, Is.EquivalentTo(destFiles)); } private void VerifyMoveFolder(string source, string from, string destination) @@ -937,7 +952,7 @@ private void VerifyMoveFolder(string source, string from, string destination) var sourceFiles = Directory.GetFileSystemEntries(source, "*", SearchOption.AllDirectories).Select(v => v.Substring(source.Length + 1)).ToArray(); var destFiles = Directory.GetFileSystemEntries(destination, "*", SearchOption.AllDirectories).Select(v => v.Substring(destination.Length + 1)).ToArray(); - CollectionAssert.AreEquivalent(sourceFiles, destFiles); + Assert.That(sourceFiles, Is.EquivalentTo(destFiles)); } private void VerifyDeletedFile(string filePath) diff --git a/src/NzbDrone.Common.Test/Http/CookieUtilFixture.cs b/src/NzbDrone.Common.Test/Http/CookieUtilFixture.cs index 970633966b8..6d778bd8d00 100644 --- a/src/NzbDrone.Common.Test/Http/CookieUtilFixture.cs +++ b/src/NzbDrone.Common.Test/Http/CookieUtilFixture.cs @@ -19,7 +19,7 @@ public void CookieHeaderToDictionaryGood() { "__cfduid", "d6237f041586694295" }, { "__cf_bm", "TlOng/xyqckk-TMen38z+0RFYA7YA=" } }; - CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader)); + Assert.That(expectedCookieDictionary, Is.EquivalentTo(CookieUtil.CookieHeaderToDictionary(cookieHeader))); } [Test] @@ -32,7 +32,7 @@ public void CookieHeaderToDictionaryDuplicateKeys() { "__cfduid", "d6237f041586694295" }, { "__cf_bm", "test" } // we always assume the latest value is the most recent }; - CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader)); + Assert.That(expectedCookieDictionary, Is.EquivalentTo(CookieUtil.CookieHeaderToDictionary(cookieHeader))); } [Test] @@ -41,7 +41,7 @@ public void CookieHeaderToDictionaryMalformed() // malformed cookies var cookieHeader = "__cfduidd6237f041586694295; __cf_;bm TlOng; good_cookie=value"; var expectedCookieDictionary = new Dictionary { { "good_cookie", "value" }, }; - CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader)); + Assert.That(expectedCookieDictionary, Is.EquivalentTo(CookieUtil.CookieHeaderToDictionary(cookieHeader))); } [Test] @@ -50,7 +50,7 @@ public void CookieHeaderToDictionaryNull() { // null cookie header var expectedCookieDictionary = new Dictionary(); - CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(null)); + Assert.That(expectedCookieDictionary, Is.EquivalentTo(CookieUtil.CookieHeaderToDictionary(null))); } [Test] @@ -63,7 +63,7 @@ public void CookieDictionaryToHeaderGood() { "__cf_bm", "TlOng/xyqckk-TMen38z+0RFYA7YA=" } }; var expectedCookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA="; - CollectionAssert.AreEqual(expectedCookieHeader, CookieUtil.CookieDictionaryToHeader(cookieDictionary)); + Assert.That(expectedCookieHeader, Is.EquivalentTo(CookieUtil.CookieDictionaryToHeader(cookieDictionary))); } [Test] @@ -75,7 +75,7 @@ public void CookieDictionaryToHeaderMalformed1() { "__cf_=bm", "34234234" } }; var ex = Assert.Throws(() => CookieUtil.CookieDictionaryToHeader(cookieDictionary)); - Assert.AreEqual("The cookie '__cf_=bm=34234234' is malformed.", ex.Message); + Assert.That("The cookie '__cf_=bm=34234234' is malformed.", Is.EqualTo(ex.Message)); } [Test] @@ -87,7 +87,7 @@ public void CookieDictionaryToHeaderMalformed2() { "__cf_bm", "34234 234" } }; var ex = Assert.Throws(() => CookieUtil.CookieDictionaryToHeader(cookieDictionary)); - Assert.AreEqual("The cookie '__cf_bm=34234 234' is malformed.", ex.Message); + Assert.That("The cookie '__cf_bm=34234 234' is malformed.", Is.EqualTo(ex.Message)); } [Test] @@ -95,7 +95,7 @@ public void CookieDictionaryToHeaderNull() { // null cookie dictionary var expectedCookieHeader = ""; - CollectionAssert.AreEqual(expectedCookieHeader, CookieUtil.CookieDictionaryToHeader(null)); + Assert.That(expectedCookieHeader, Is.EquivalentTo(CookieUtil.CookieDictionaryToHeader(null))); } } } diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index b6c9b68e8ee..a39c2d83bba 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -5,7 +5,9 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text; using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using Moq; using NLog; @@ -362,6 +364,28 @@ public void should_send_headers(string header, string value) response.Resource.Headers[header].ToString().Should().Be(value); } + [Test] + public async Task should_send_basic_auth_header_as_utf8() + { + var username = "tèst"; + var password = "pâsswörd_ș"; + + var request = new HttpRequest($"https://{_httpBinHost}/get") + { + Credentials = new BasicNetworkCredential(username, password) + }; + + var response = await Subject.GetAsync(request); + + response.Resource.Headers.Should().ContainKey("Authorization"); + + var authHeader = response.Resource.Headers["Authorization"].ToString(); + var encodedPart = authHeader!.Split(' ', 2).Last(); + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encodedPart)); + + decoded.Should().Be($"{username}:{password}"); + } + [Test] public void should_download_file() { diff --git a/src/NzbDrone.Common.Test/NetworkTests/IPNetworkParserFixture.cs b/src/NzbDrone.Common.Test/NetworkTests/IPNetworkParserFixture.cs new file mode 100644 index 00000000000..68a63d1a95b --- /dev/null +++ b/src/NzbDrone.Common.Test/NetworkTests/IPNetworkParserFixture.cs @@ -0,0 +1,112 @@ +using System.Net; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Network; + +namespace NzbDrone.Common.Test.NetworkTests +{ + [TestFixture] + public class IPNetworkParserFixture + { + [TestCase("10.0.0.0/8", "10.0.0.0", 8)] + [TestCase("172.16.0.0/12", "172.16.0.0", 12)] + [TestCase("192.168.0.0/16", "192.168.0.0", 16)] + [TestCase("10.0.0.5/32", "10.0.0.5", 32)] + [TestCase("fc00::/7", "fc00::", 7)] + [TestCase("fe80::/10", "fe80::", 10)] + [TestCase("2001:db8::1/128", "2001:db8::1", 128)] + public void should_parse_network_in_cidr_notation(string value, string expectedAddress, int expectedPrefixLength) + { + IPNetworkParser.TryParse(value, out var address, out var prefixLength).Should().BeTrue(); + + address.Should().Be(IPAddress.Parse(expectedAddress)); + prefixLength.Should().Be(expectedPrefixLength); + } + + [TestCase("172.17.0.1", "172.17.0.1", 32)] + [TestCase("2001:db8::1", "2001:db8::1", 128)] + public void should_use_single_host_prefix_length_for_bare_address(string value, string expectedAddress, int expectedPrefixLength) + { + IPNetworkParser.TryParse(value, out var address, out var prefixLength).Should().BeTrue(); + + address.Should().Be(IPAddress.Parse(expectedAddress)); + prefixLength.Should().Be(expectedPrefixLength); + } + + [TestCase(" 10.0.0.0/8 ")] + [TestCase("10.0.0.0 / 8")] + [TestCase("\t172.17.0.1")] + public void should_ignore_surrounding_whitespace(string value) + { + IPNetworkParser.TryParse(value, out _, out _).Should().BeTrue(); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("not-an-address")] + [TestCase("10.0.0.0/")] + [TestCase("/8")] + [TestCase("10.0.0.0/8/8")] + [TestCase("10.0.0.0/eight")] + [TestCase("10.0.0.0/-1")] + [TestCase("10.0.0.0/33")] + [TestCase("fc00::/-1")] + [TestCase("fc00::/129")] + [TestCase("10.0.0.0/128")] + [TestCase("0.0.0.0/0")] + [TestCase("::/0")] + [TestCase("10.0.0.0/0")] + [TestCase("10")] + [TestCase("10/8")] + [TestCase("10.1/8")] + [TestCase("10.1.2/8")] + [TestCase("10.1.2.3/8")] + [TestCase("172.17.0.0/12")] + [TestCase("192.168.1.1/24")] + [TestCase("fc00::1/7")] + [TestCase("2001:db8::1/32")] + [TestCase("example.com")] + [TestCase("example.com/24")] + public void should_not_parse_invalid_network(string value) + { + IPNetworkParser.TryParse(value, out var address, out var prefixLength).Should().BeFalse(); + + address.Should().BeNull(); + prefixLength.Should().Be(0); + } + + [TestCase("10.0.0.0/8", true)] + [TestCase("10.0.0.0/8,192.168.0.0/16", true)] + [TestCase("10.0.0.0/8, 172.17.0.1", true)] + [TestCase(null, true)] + [TestCase("", true)] + [TestCase(" ", true)] + [TestCase("10.0.0.0/8,garbage", false)] + [TestCase("garbage", false)] + [TestCase("10.0.0.0/8,", true)] + [TestCase(",10.0.0.0/8", true)] + [TestCase("10.0.0.0/8, ,192.168.0.0/16", true)] + [TestCase(",,", true)] + [TestCase("0.0.0.0/0", false)] + [TestCase("10.0.0.0/8,10.1.2.3/8", false)] + public void should_validate_list(string value, bool expected) + { + IPNetworkParser.IsValidList(value).Should().Be(expected); + } + + [TestCase("10.0.0.0/8,", "10.0.0.0/8")] + [TestCase(",10.0.0.0/8", "10.0.0.0/8")] + [TestCase("10.0.0.0/8,,192.168.0.0/16", "10.0.0.0/8, 192.168.0.0/16")] + [TestCase(" 10.0.0.0/8 , 172.17.0.1 ", "10.0.0.0/8, 172.17.0.1")] + [TestCase("10.0.0.0/8", "10.0.0.0/8")] + [TestCase(",,", "")] + [TestCase(null, "")] + [TestCase("", "")] + [TestCase(" ", "")] + public void should_normalize_list(string value, string expected) + { + IPNetworkParser.NormalizeList(value).Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Common/ArchiveService.cs b/src/NzbDrone.Common/ArchiveService.cs index d420bbbc0a0..e61472120aa 100644 --- a/src/NzbDrone.Common/ArchiveService.cs +++ b/src/NzbDrone.Common/ArchiveService.cs @@ -87,6 +87,13 @@ private void ExtractZip(string compressedFile, string destination) // Manipulate the output filename here as desired. var fullZipToPath = Path.Combine(destination, entryFileName); var directoryName = Path.GetDirectoryName(fullZipToPath); + + // Ensure the destination path is within the target directory + if (!Path.GetFullPath(fullZipToPath).StartsWith(Path.GetFullPath(destination).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar)) + { + throw new IOException($"Entry is outside the target dir: {entryFileName}"); + } + if (directoryName.Length > 0) { Directory.CreateDirectory(directoryName); diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index fb7d93f48ca..d3ab34bf9e9 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -28,15 +28,15 @@ public DiskTransferService(IDiskProvider diskProvider, Logger logger) private string ResolveRealParentPath(string path) { - var parentPath = path.GetParentPath(); - if (!_diskProvider.FolderExists(parentPath)) + if (!_diskProvider.FolderExists(path.GetParentPath())) { return path; } + var cleanPath = path.GetCleanPath(); + var parentPath = cleanPath.GetParentPath(); var realParentPath = parentPath.GetActualCasing(); - - var partialChildPath = path.Substring(parentPath.Length); + var partialChildPath = cleanPath.Substring(parentPath.Length); return realParentPath + partialChildPath; } diff --git a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs index f74ac685ab3..e9c9bebbb31 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs @@ -9,6 +9,8 @@ public interface IRuntimeInfo bool IsAdmin { get; } bool IsWindowsService { get; } bool IsWindowsTray { get; } + bool IsSystemdService { get; } + bool IsContainerized { get; } bool IsStarting { get; set; } bool IsExiting { get; set; } bool IsTray { get; } diff --git a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs index aece2785955..2015e2e2b80 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs @@ -17,6 +17,8 @@ public class OsInfo : IOsInfo // this needs to not be static so we can mock it public bool IsDocker { get; } + public bool IsPodman { get; } + public bool IsContainerized { get; } public string Version { get; } public string Name { get; } @@ -79,11 +81,14 @@ public OsInfo(IEnumerable versionAdapters, Logger logger) FullName = Name; } - if (IsLinux && - (File.Exists("/.dockerenv") || - (File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/")))) + if (IsLinux) { - IsDocker = true; + IsDocker = File.Exists("/.dockerenv") || + (File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/")); + IsPodman = File.Exists("/run/.containerenv") || + Environment.GetEnvironmentVariable("container") != null; + + IsContainerized = IsDocker || IsPodman || Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is "true" or "1"; } } } @@ -94,6 +99,8 @@ public interface IOsInfo string Name { get; } string FullName { get; } bool IsDocker { get; } + bool IsPodman { get; } + bool IsContainerized { get; } } public enum Os diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index ff7f6c61c6a..be386a9077e 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -12,11 +12,13 @@ namespace NzbDrone.Common.EnvironmentInfo public class RuntimeInfo : IRuntimeInfo { private readonly Logger _logger; + private readonly IOsInfo _osInfo; private readonly DateTime _startTime = DateTime.UtcNow; - public RuntimeInfo(Logger logger, IHostLifetime hostLifetime = null) + public RuntimeInfo(Logger logger, IOsInfo osInfo, IHostLifetime hostLifetime = null) { _logger = logger; + _osInfo = osInfo; IsWindowsService = hostLifetime is WindowsServiceLifetime; IsStarting = true; @@ -83,6 +85,30 @@ public bool IsAdmin public bool IsWindowsService { get; private set; } + public bool IsContainerized => _osInfo.IsContainerized; + + public bool IsSystemdService + { + get + { + if (!OsInfo.IsLinux) + { + return false; + } + + try + { + var invocationId = Environment.GetEnvironmentVariable("INVOCATION_ID"); + return !string.IsNullOrEmpty(invocationId); + } + catch (Exception ex) + { + _logger.Warn(ex, "Error checking if system is running under systemd"); + return false; + } + } + } + public bool IsStarting { get; set; } public bool IsExiting { get; set; } public bool IsTray diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 6c312523573..4c4b449f699 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -78,7 +78,7 @@ public async Task GetResponseAsync(HttpRequest request, CookieCont { // Manually set header to avoid initial challenge response var authInfo = bc.UserName + ":" + bc.Password; - authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo)); + authInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes(authInfo)); requestMessage.Headers.Add("Authorization", "Basic " + authInfo); } else if (request.Credentials is NetworkCredential nc) diff --git a/src/NzbDrone.Common/Http/HttpResponse.cs b/src/NzbDrone.Common/Http/HttpResponse.cs index 3e0e6e2a127..8d12e92a881 100644 --- a/src/NzbDrone.Common/Http/HttpResponse.cs +++ b/src/NzbDrone.Common/Http/HttpResponse.cs @@ -138,12 +138,14 @@ public override string ToString() public class HttpResponse : HttpResponse where T : new() { + private readonly Lazy _resource; + public HttpResponse(HttpResponse response) : base(response.Request, response.Headers, response.Cookies, response.ResponseData, response.ElapsedTime, response.StatusCode, response.Version) { - Resource = Json.Deserialize(response.Content); + _resource = new Lazy(() => Json.Deserialize(response.Content)); } - public T Resource { get; private set; } + public T Resource => _resource.Value; } } diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index 36557b62a0f..a1d26c03752 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -15,7 +15,7 @@ public static class NzbDroneLogger private const string FileLogLayout = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; private const string ConsoleFormat = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; - private static readonly CleansingConsoleLogLayout CleansingConsoleLayout = new(ConsoleFormat); + private static readonly CleansingConsoleLogLayout CleansingConsoleLayout = new(ConsoleFormat); private static readonly CleansingClefLogLayout ClefLogLayout = new(); private static bool _isConfigured; @@ -200,6 +200,8 @@ private static void RegisterGlobalFilters() { c.ForLogger("System.*").WriteToNil(LogLevel.Warn); c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn); + c.ForLogger("Microsoft.AspNetCore.HostFiltering*").WriteToNil(LogLevel.Info); + c.ForLogger("Microsoft.AspNetCore.HttpOverrides*").WriteToNil(LogLevel.Debug); c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info); c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal); c.ForLogger("Prowlarr.Http.Authentication.ApiKeyAuthenticationHandler").WriteToNil(LogLevel.Info); @@ -224,6 +226,13 @@ public static void ConfigureConsoleLayout(ColoredConsoleTarget target, ConsoleLo _ => NzbDroneLogger.CleansingConsoleLayout }; } + + public static void ResetAllTargets(IStartupContext startupContext, bool updateApp, bool inConsole) + { + LogManager.Configuration = new LoggingConfiguration(); + _isConfigured = false; + Register(startupContext, updateApp, inConsole); + } } public enum ConsoleLogFormat diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index 3a4737f743f..1fde8826284 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -178,7 +178,7 @@ public void UpdateScope(IOsInfo osInfo) { SentrySdk.ConfigureScope(scope => { - scope.SetTag("is_docker", $"{osInfo.IsDocker}"); + scope.SetTag("is_containerized", $"{osInfo.IsContainerized}"); if (osInfo.Name != null && !OsInfo.IsWindows) { diff --git a/src/NzbDrone.Common/Messaging/LifecycleEventAttribute.cs b/src/NzbDrone.Common/Messaging/LifecycleEventAttribute.cs new file mode 100644 index 00000000000..b2101ff3ef9 --- /dev/null +++ b/src/NzbDrone.Common/Messaging/LifecycleEventAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace NzbDrone.Common.Messaging +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class LifecycleEventAttribute : Attribute + { + } +} diff --git a/src/NzbDrone.Common/Network/IPNetworkParser.cs b/src/NzbDrone.Common/Network/IPNetworkParser.cs new file mode 100644 index 00000000000..ce735b14d4b --- /dev/null +++ b/src/NzbDrone.Common/Network/IPNetworkParser.cs @@ -0,0 +1,126 @@ +using System.Linq; +using System.Net; +using System.Net.Sockets; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Common.Network +{ + public static class IPNetworkParser + { + public static bool TryParse(string value, out IPAddress address, out int prefixLength) + { + address = null; + prefixLength = 0; + + if (value.IsNullOrWhiteSpace()) + { + return false; + } + + var parts = value.Split('/'); + + if (parts.Length > 2) + { + return false; + } + + var addressPart = parts[0].Trim(); + + if (!IPAddress.TryParse(addressPart, out var parsedAddress)) + { + return false; + } + + if (parsedAddress.AddressFamily == AddressFamily.InterNetwork && addressPart.Count(c => c == '.') != 3) + { + return false; + } + + var maxPrefixLength = parsedAddress.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32; + + if (parts.Length == 1) + { + address = parsedAddress; + prefixLength = maxPrefixLength; + + return true; + } + + if (!int.TryParse(parts[1].Trim(), out var parsedPrefixLength) || + parsedPrefixLength < 1 || + parsedPrefixLength > maxPrefixLength) + { + return false; + } + + if (HasHostBitsSet(parsedAddress, parsedPrefixLength)) + { + return false; + } + + address = parsedAddress; + prefixLength = parsedPrefixLength; + + return true; + } + + public static bool IsValidList(string value) + { + if (value.IsNullOrWhiteSpace()) + { + return true; + } + + foreach (var entry in value.Split(',')) + { + if (entry.IsNullOrWhiteSpace()) + { + continue; + } + + if (!TryParse(entry, out _, out _)) + { + return false; + } + } + + return true; + } + + public static string NormalizeList(string value) + { + if (value.IsNullOrWhiteSpace()) + { + return string.Empty; + } + + return string.Join(", ", value.Split(',') + .Select(e => e.Trim()) + .Where(e => e.IsNotNullOrWhiteSpace())); + } + + private static bool HasHostBitsSet(IPAddress address, int prefixLength) + { + var bytes = address.GetAddressBytes(); + + for (var i = 0; i < bytes.Length; i++) + { + var prefixBitsInByte = prefixLength - (i * 8); + + if (prefixBitsInByte >= 8) + { + continue; + } + + var hostMask = prefixBitsInByte <= 0 ? 0xFF : 0xFF >> prefixBitsInByte; + + if ((bytes[i] & hostMask) != 0) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/NzbDrone.Common/Options/ServerOptions.cs b/src/NzbDrone.Common/Options/ServerOptions.cs index d21e12b2a2e..8a4933559c6 100644 --- a/src/NzbDrone.Common/Options/ServerOptions.cs +++ b/src/NzbDrone.Common/Options/ServerOptions.cs @@ -4,9 +4,11 @@ public class ServerOptions { public string UrlBase { get; set; } public string BindAddress { get; set; } + public string AllowedHosts { 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; } + public string TrustedNetworks { get; set; } } diff --git a/src/NzbDrone.Common/Prowlarr.Common.csproj b/src/NzbDrone.Common/Prowlarr.Common.csproj index 6495bfe2398..17545921af3 100644 --- a/src/NzbDrone.Common/Prowlarr.Common.csproj +++ b/src/NzbDrone.Common/Prowlarr.Common.csproj @@ -13,15 +13,14 @@ - + - - + + - diff --git a/src/NzbDrone.Core.Test/Configuration/AllowedHostsParserFixture.cs b/src/NzbDrone.Core.Test/Configuration/AllowedHostsParserFixture.cs new file mode 100644 index 00000000000..bbb77bfc31f --- /dev/null +++ b/src/NzbDrone.Core.Test/Configuration/AllowedHostsParserFixture.cs @@ -0,0 +1,87 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Configuration; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Configuration +{ + [TestFixture] + public class AllowedHostsParserFixture : TestBase + { + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void should_return_empty_list_for_null_or_empty(string value) + { + AllowedHostsParser.Parse(value).Should().BeEmpty(); + } + + [Test] + public void should_split_on_comma_and_semicolon_and_trim_whitespace() + { + var result = AllowedHostsParser.Parse("prowlarr.local, 192.168.1.5; myhost"); + + result.Should().BeEquivalentTo("prowlarr.local", "192.168.1.5", "myhost"); + } + + [Test] + public void should_wrap_bare_ipv6_addresses_in_brackets() + { + var result = AllowedHostsParser.Parse("::1, 2001:db8::1, [fe80::1]"); + + result.Should().BeEquivalentTo("[::1]", "[2001:db8::1]", "[fe80::1]"); + } + + [Test] + public void should_not_duplicate_bare_and_bracketed_ipv6_addresses() + { + var result = AllowedHostsParser.Parse("::1,[::1]"); + + result.Should().BeEquivalentTo("[::1]"); + } + + [Test] + public void should_remove_empty_entries_and_duplicates() + { + var result = AllowedHostsParser.Parse("a,,b, a "); + + result.Should().BeEquivalentTo("a", "b"); + } + + [TestCase("prowlarr.local")] + [TestCase("prowlarr.example.co.uk")] + [TestCase("my-host")] + [TestCase("192.168.1.5")] + [TestCase("[::1]")] + [TestCase("[2001:db8::1]")] + [TestCase("2001:db8::1")] + [TestCase("*.example.com")] + [TestCase("*.example.co.uk")] + [TestCase("*.subdomain.example.com")] + [TestCase("*.subdomain.example.co.uk")] + public void should_be_valid_host(string host) + { + AllowedHostsParser.IsValidHost(host).Should().BeTrue(); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("http://prowlarr.local")] + [TestCase("prowlarr.local:8989")] + [TestCase("prowlarr local")] + [TestCase("prowlarr.*")] + [TestCase("*")] + [TestCase("*.")] + [TestCase("*.192.168.1.5")] + [TestCase("*.[::1]")] + [TestCase("*.*.example.com")] + [TestCase("[not-an-address]")] + [TestCase(".example.com")] + [TestCase(".example.co.uk")] + public void should_not_be_valid_host(string host) + { + AllowedHostsParser.IsValidHost(host).Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/AllowedHostsCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/AllowedHostsCheckFixture.cs new file mode 100644 index 00000000000..0581a50a89b --- /dev/null +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/AllowedHostsCheckFixture.cs @@ -0,0 +1,89 @@ +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.HealthCheck.Checks; +using NzbDrone.Core.Localization; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.HealthCheck.Checks +{ + [TestFixture] + public class AllowedHostsCheckFixture : CoreTest + { + [SetUp] + public void Setup() + { + Mocker.GetMock() + .Setup(s => s.GetLocalizedString(It.IsAny())) + .Returns("Some Warning Message"); + + GivenAllowedHosts(string.Empty); + GivenAuthenticationRequired(AuthenticationRequiredType.Enabled); + } + + private void GivenAllowedHosts(string allowedHosts) + { + Mocker.GetMock() + .SetupGet(s => s.AllowedHosts) + .Returns(allowedHosts); + } + + private void GivenAuthenticationRequired(AuthenticationRequiredType required) + { + Mocker.GetMock() + .SetupGet(s => s.AuthenticationRequired) + .Returns(required); + } + + [Test] + public void should_return_ok_when_authentication_is_required() + { + Subject.Check().ShouldBeOk(); + } + + [Test] + public void should_return_warning_when_authentication_is_not_required_for_local_addresses() + { + GivenAuthenticationRequired(AuthenticationRequiredType.DisabledForLocalAddresses); + + Subject.Check().ShouldBeWarning(); + + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + public void should_return_ok_when_allowed_hosts_is_configured() + { + GivenAllowedHosts("prowlarr.local"); + GivenAuthenticationRequired(AuthenticationRequiredType.DisabledForLocalAddresses); + + Subject.Check().ShouldBeOk(); + } + + [TestCase("*")] + [TestCase("*, prowlarr.local")] + [TestCase("prowlarr.local; *")] + public void should_return_warning_when_allowed_hosts_contains_wildcard(string allowedHosts) + { + GivenAllowedHosts(allowedHosts); + GivenAuthenticationRequired(AuthenticationRequiredType.DisabledForLocalAddresses); + + Subject.Check().ShouldBeWarning(); + + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + public void should_return_warning_when_allowed_hosts_is_whitespace() + { + GivenAllowedHosts(" , "); + GivenAuthenticationRequired(AuthenticationRequiredType.DisabledForLocalAddresses); + + Subject.Check().ShouldBeWarning(); + + ExceptionVerification.ExpectedWarns(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/HealthCheck/HealthCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/HealthCheckFixture.cs index cd9b301639e..e86e872e876 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/HealthCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/HealthCheckFixture.cs @@ -15,7 +15,7 @@ public class HealthCheckFixture : CoreTest [TestCase("I blew up because of some weird user mistake", "custom-page#my-health-check", WikiRoot + "prowlarr/custom-page#my-health-check")] public void should_format_wiki_url(string message, string wikiFragment, string expectedUrl) { - var subject = new NzbDrone.Core.HealthCheck.HealthCheck(typeof(HealthCheckBase), HealthCheckResult.Warning, message, wikiFragment); + var subject = new NzbDrone.Core.HealthCheck.HealthCheck(typeof(HealthCheckBase), HealthCheckResult.Warning, HealthCheckReason.ServerNotification, message, wikiFragment); subject.WikiUrl.Should().Be(expectedUrl); } diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs index e67fda3f34f..94cd9c6eb58 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using FluentAssertions; using Moq; using NUnit.Framework; @@ -33,11 +32,11 @@ private List WatchForSearchCriteria() _mockIndexer.Setup(v => v.Fetch(It.IsAny())) .Callback(s => result.Add(s)) - .Returns(Task.FromResult(new IndexerPageableQueryResult())); + .ReturnsAsync(new IndexerPageableQueryResult()); _mockIndexer.Setup(v => v.Fetch(It.IsAny())) .Callback(s => result.Add(s)) - .Returns(Task.FromResult(new IndexerPageableQueryResult())); + .ReturnsAsync(new IndexerPageableQueryResult()); return result; } diff --git a/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs index ae7eaa76257..2b51e929d20 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs @@ -41,7 +41,7 @@ public async Task should_parse_recent_feed_from_animebytes() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new BasicSearchCriteria { SearchTerm = "test", Categories = new[] { 2000, 5000 } })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/AvistazFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/AvistazFixture.cs index dcabb4dbf0d..a25194c1ffc 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/AvistazFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/AvistazFixture.cs @@ -36,7 +36,7 @@ public async Task should_parse_recent_feed_from_AvistaZ() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/ExoticazFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/ExoticazFixture.cs index fe4ada593a1..33d3c09e5ec 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/ExoticazFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/ExoticazFixture.cs @@ -36,7 +36,7 @@ public async Task should_parse_recent_feed_from_ExoticaZ() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/PrivateHDFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/PrivateHDFixture.cs index f0531e9ec88..695d5ab3ddc 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/PrivateHDFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/PrivateHDFixture.cs @@ -36,7 +36,7 @@ public async Task should_parse_recent_feed_from_PrivateHD() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs index 691df62d172..883e8ad0109 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs @@ -91,15 +91,16 @@ public void should_search_by_tvdbid_season_if_supported() secondQuery.Name.Should().Be("S01E%"); } - [Test] - public void should_search_by_tvdbid_season_episode_if_supported() + [TestCase(1, "3", "S01E%03%")] + [TestCase(1, "103", "S01E103%")] + public void should_search_by_tvdbid_season_episode_if_supported(int season, string episode, string expectedQuery) { var tvSearchCriteria = new TvSearchCriteria { Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, TvdbId = 371980, - Season = 1, - Episode = "3" + Season = season, + Episode = episode }; var results = Subject.GetSearchRequests(tvSearchCriteria); @@ -114,7 +115,7 @@ public void should_search_by_tvdbid_season_episode_if_supported() query.Tvrage.Should().BeNull(); query.Search.Should().BeNull(); query.Category.Should().Be("Episode"); - query.Name.Should().Be("S01E03%"); + query.Name.Should().Be(expectedQuery); } [Test] @@ -226,15 +227,16 @@ public void should_search_by_term_season_if_supported() secondQuery.Name.Should().Be("S02E%"); } - [Test] - public void should_search_by_term_season_episode_if_supported() + [TestCase(2, "3", "S02E%03%")] + [TestCase(2, "103", "S02E103%")] + public void should_search_by_term_season_episode_if_supported(int season, string episode, string expectedQuery) { var tvSearchCriteria = new TvSearchCriteria { Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, SearchTerm = "Malcolm in the Middle", - Season = 2, - Episode = "3" + Season = season, + Episode = episode }; var results = Subject.GetSearchRequests(tvSearchCriteria); @@ -249,7 +251,7 @@ public void should_search_by_term_season_episode_if_supported() query.Tvrage.Should().BeNull(); query.Search.Should().Be("Malcolm%in%the%Middle"); query.Category.Should().Be("Episode"); - query.Name.Should().Be("S02E03%"); + query.Name.Should().Be(expectedQuery); } [Test] diff --git a/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs index 4bc704ccde6..df2aa8b620a 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs @@ -36,7 +36,7 @@ public async Task should_parse_recent_feed_from_GazelleGames() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases; @@ -73,7 +73,7 @@ public async Task should_not_error_if_empty_response() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs index 06326d162e9..5ff877027ae 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs @@ -47,7 +47,7 @@ public async Task should_parse_recent_feed_from_HDBits(string fileName) Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Post), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson)); var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases; @@ -76,7 +76,7 @@ public async Task should_warn_on_wrong_passkey() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.IsAny(), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson)))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson))); var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs index 084767ffa29..ff42d2f7107 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs @@ -49,7 +49,7 @@ public async Task should_parse_recent_feed_from_newznab_nzb_su() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 }, Limit = 100, Offset = 0 })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/NzbIndexTests/NzbIndexFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NzbIndexTests/NzbIndexFixture.cs index 7691e37e342..4f7c4757e05 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NzbIndexTests/NzbIndexFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NzbIndexTests/NzbIndexFixture.cs @@ -37,7 +37,7 @@ public async Task should_parse_search_results() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), responseJson))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), responseJson)); var releases = (await Subject.Fetch(new BasicSearchCriteria { SearchTerm = "test" })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs index d33dde2d37d..b87ca435b9e 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs @@ -35,7 +35,7 @@ public async Task should_parse_recent_feed_from_Orpheus() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs index 17859b7960d..0603b7b70f4 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs @@ -38,7 +38,7 @@ public async Task should_parse_feed_from_PTP(string fileName) Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson)); var torrents = (await Subject.Fetch(new MovieSearchCriteria())).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs index 02a205b272a..f7a5138b5c4 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs @@ -42,7 +42,7 @@ public async Task should_parse_recent_feed_from_Rarbg() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases; @@ -69,7 +69,7 @@ public async Task should_parse_error_20_as_empty_results() { Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }"))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }")); var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases; @@ -81,7 +81,7 @@ public async Task should_warn_on_unknown_error() { Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }"))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }")); var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs index d7eb35cd153..27da2d83094 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs @@ -35,7 +35,7 @@ public async Task should_parse_recent_feed_from_Redacted() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/SecretCinemaTests/SecretCinemaFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SecretCinemaTests/SecretCinemaFixture.cs index 7c5166d2670..e3f82d557eb 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/SecretCinemaTests/SecretCinemaFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/SecretCinemaTests/SecretCinemaFixture.cs @@ -36,7 +36,7 @@ public async Task should_parse_recent_feed_from_SecretCinema() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases; diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs index a5fdf0506cb..adb5cd791c9 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs @@ -53,7 +53,7 @@ public async Task should_parse_recent_feed_from_torznab_hdaccess_net() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases; @@ -82,7 +82,7 @@ public async Task should_parse_recent_feed_from_torznab_tpb() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases; @@ -96,7 +96,7 @@ public async Task should_parse_recent_feed_from_torznab_animetosho() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases; @@ -127,7 +127,7 @@ public async Task should_parse_recent_feed_from_torznab_morethantv() Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) - .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); + .ReturnsAsync((HttpRequest r, IndexerDefinition _) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)); var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases; diff --git a/src/NzbDrone.Core/Configuration/AllowedHostsParser.cs b/src/NzbDrone.Core/Configuration/AllowedHostsParser.cs new file mode 100644 index 00000000000..bee93c10bb5 --- /dev/null +++ b/src/NzbDrone.Core/Configuration/AllowedHostsParser.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Configuration +{ + public static class AllowedHostsParser + { + private static readonly char[] Separators = { ',', ';' }; + + public static List Parse(string value) + { + if (value.IsNullOrWhiteSpace()) + { + return new List(); + } + + return value.Split(Separators) + .Select(h => Normalize(h.Trim())) + .Where(h => h.IsNotNullOrWhiteSpace()) + .Distinct() + .ToList(); + } + + public static bool IsValidHost(string host) + { + if (host.IsNullOrWhiteSpace()) + { + return false; + } + + if (host.StartsWith("*.")) + { + return Uri.CheckHostName(host.Substring(2)) == UriHostNameType.Dns; + } + + return Uri.CheckHostName(host) != UriHostNameType.Unknown; + } + + private static string Normalize(string host) + { + if (!IsBracketed(host) && host.Contains(':') && host.IsValidIpAddress()) + { + return $"[{host}]"; + } + + return host; + } + + private static bool IsBracketed(string host) + { + return host.StartsWith("[") && host.EndsWith("]"); + } + } +} diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 7f68071a9aa..ebae10a7bf7 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -36,6 +36,7 @@ public interface IConfigFileProvider : IHandleAsync, bool LaunchBrowser { get; } AuthenticationType AuthenticationMethod { get; } AuthenticationRequiredType AuthenticationRequired { get; } + string AllowedHosts { get; } bool AnalyticsEnabled { get; } string LogLevel { get; } string ConsoleLogLevel { get; } @@ -49,6 +50,7 @@ public interface IConfigFileProvider : IHandleAsync, string SslCertPath { get; } string SslCertPassword { get; } string UrlBase { get; } + string TrustedNetworks { get; } string UiFolder { get; } string InstanceName { get; } bool UpdateAutomatically { get; } @@ -235,6 +237,10 @@ public AuthenticationType AuthenticationMethod ? enumValue : GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); + public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false); + + public string AllowedHosts => _serverOptions.AllowedHosts ?? GetValue("AllowedHosts", string.Empty); + public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false); public string Branch => _updateOptions.Branch ?? GetValue("Branch", "master").ToLowerInvariant(); @@ -280,6 +286,8 @@ public string UrlBase } } + public string TrustedNetworks => _serverOptions.TrustedNetworks ?? GetValue("TrustedNetworks", string.Empty); + public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI"; public string InstanceName @@ -500,7 +508,5 @@ public void Execute(ResetApiKeyCommand message) SetValue("ApiKey", GenerateApiKey()); _eventAggregator.PublishEvent(new ApiKeyChangedEvent()); } - - public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false); } } diff --git a/src/NzbDrone.Core/Datastore/Migration/043_newznab_indexers_enable_redirect.cs b/src/NzbDrone.Core/Datastore/Migration/043_newznab_indexers_enable_redirect.cs index 4411e226e93..6375c35bd92 100644 --- a/src/NzbDrone.Core/Datastore/Migration/043_newznab_indexers_enable_redirect.cs +++ b/src/NzbDrone.Core/Datastore/Migration/043_newznab_indexers_enable_redirect.cs @@ -1,5 +1,3 @@ -using System.Data; -using Dapper; using FluentMigrator; using NzbDrone.Core.Datastore.Migration.Framework; @@ -10,13 +8,7 @@ public class newznab_indexers_enable_redirect : NzbDroneMigrationBase { protected override void MainDbUpgrade() { - Execute.WithConnection(UpdateNewznabRedirectSetting); - } - - private void UpdateNewznabRedirectSetting(IDbConnection conn, IDbTransaction tran) - { - var updateSql = "UPDATE \"Indexers\" SET \"Redirect\" = @Redirect WHERE \"Implementation\" = 'Newznab' AND \"Redirect\" = false"; - conn.Execute(updateSql, new { Redirect = true }, transaction: tran); + Update.Table("Indexers").Set(new { Redirect = true }).Where(new { Implementation = "Newznab", Redirect = false }); } } } diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index 217e70a31cb..d9e1b599aca 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -168,7 +168,7 @@ private string DownloadFromMagnetUrl(TorrentInfo release, string magnetUrl) try { - hash = MagnetLink.Parse(magnetUrl).InfoHash.ToHex(); + hash = MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex(); } catch (FormatException ex) { diff --git a/src/NzbDrone.Core/Download/TorrentFileInfoReader.cs b/src/NzbDrone.Core/Download/TorrentFileInfoReader.cs index ef944cc1bcf..f31b4acbf79 100644 --- a/src/NzbDrone.Core/Download/TorrentFileInfoReader.cs +++ b/src/NzbDrone.Core/Download/TorrentFileInfoReader.cs @@ -11,7 +11,7 @@ public class TorrentFileInfoReader : ITorrentFileInfoReader { public string GetHashFromTorrentFile(byte[] fileContents) { - return Torrent.Load(fileContents).InfoHash.ToHex(); + return Torrent.Load(fileContents).InfoHashes.V1OrV2.ToHex(); } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/AllowedHostsCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/AllowedHostsCheck.cs new file mode 100644 index 00000000000..e2c2cd49d8f --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/AllowedHostsCheck.cs @@ -0,0 +1,47 @@ +using NLog; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration.Events; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Localization; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(ApplicationStartedEvent))] + [CheckOn(typeof(ConfigFileSavedEvent))] + public class AllowedHostsCheck : HealthCheckBase + { + private readonly IConfigFileProvider _configFileProvider; + private readonly Logger _logger; + + public AllowedHostsCheck(IConfigFileProvider configFileProvider, ILocalizationService localizationService, Logger logger) + : base(localizationService) + { + _configFileProvider = configFileProvider; + _logger = logger; + } + + public override HealthCheck Check() + { + var allowedHosts = AllowedHostsParser.Parse(_configFileProvider.AllowedHosts); + + if (allowedHosts.Count > 0 && !allowedHosts.Contains("*")) + { + return new HealthCheck(GetType()); + } + + if (_configFileProvider.AuthenticationRequired != AuthenticationRequiredType.Enabled) + { + _logger.Warn("Allowed Hosts is not configured, requests for any hostname will be accepted. You can set this via settings or the config file"); + + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + HealthCheckReason.AllowedHostsNotConfigured, + _localizationService.GetLocalizedString("AllowedHostsNotConfiguredHealthCheckMessage"), + "#allowed-hosts-not-configured"); + } + + return new HealthCheck(GetType()); + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs index d3198b11172..9dc6c9c9a80 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs @@ -29,7 +29,12 @@ public override HealthCheck Check() { _logger.Warn("Please update your API key to be at least {0} characters long. You can do this via settings or the config file", MinimumLength); - return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage", new Dictionary { { "length", MinimumLength } }), "#invalid-api-key"); + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + HealthCheckReason.MinimumApiKeyLength, + _localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage", + new Dictionary { { "length", MinimumLength } }), + "#invalid-api-key"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs index a322ea08539..15440de6c0a 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs @@ -19,7 +19,11 @@ public override HealthCheck Check() if (_appFolderInfo.StartUpFolder.IsParentPath(_appFolderInfo.AppDataFolder) || _appFolderInfo.StartUpFolder.PathEquals(_appFolderInfo.AppDataFolder)) { - return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("AppDataLocationHealthCheckMessage")); + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + HealthCheckReason.AppDataLocation, + _localizationService.GetLocalizedString("AppDataLocationHealthCheckMessage"), + "#updating-will-not-be-possible-to-prevent-deleting-appdata-on-update"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ApplicationLongTermStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ApplicationLongTermStatusCheck.cs index 59dc79c5b1b..d6ca276ee79 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ApplicationLongTermStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ApplicationLongTermStatusCheck.cs @@ -46,12 +46,14 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.ApplicationLongTermStatusAllUnavailable, _localizationService.GetLocalizedString("ApplicationLongTermStatusCheckAllClientMessage"), "#applications-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, + HealthCheckReason.ApplicationLongTermStatusUnavailable, string.Format(_localizationService.GetLocalizedString("ApplicationLongTermStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#applications-are-unavailable-due-to-failures"); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ApplicationStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ApplicationStatusCheck.cs index 5d9c87184ca..4886d1ef3a6 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ApplicationStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ApplicationStatusCheck.cs @@ -44,12 +44,14 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.ApplicationStatusAllClients, _localizationService.GetLocalizedString("ApplicationStatusCheckAllClientMessage"), "#applications-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, + HealthCheckReason.ApplicationStatusSingleClient, string.Format(_localizationService.GetLocalizedString("ApplicationStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#applications-are-unavailable-due-to-failures"); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs index 6be6f572dc6..0624bee78e5 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs @@ -42,12 +42,14 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.DownloadClientStatusAllClients, _localizationService.GetLocalizedString("DownloadClientStatusAllClientHealthCheckMessage"), "#download-clients-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, + HealthCheckReason.DownloadClientStatusSingleClient, _localizationService.GetLocalizedString("DownloadClientStatusSingleClientHealthCheckMessage", new Dictionary { { "downloadClientNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs index 0a163045e2e..98bc33c0d3d 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs @@ -27,7 +27,10 @@ public override HealthCheck Check() if (enabled.Empty()) { - return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("IndexerHealthCheckNoIndexers")); + return new HealthCheck(GetType(), + HealthCheckResult.Error, + HealthCheckReason.IndexerCheckNoAvailableIndexers, + _localizationService.GetLocalizedString("IndexerHealthCheckNoIndexers")); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs index 59230460630..5b08cf002f4 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs @@ -40,6 +40,7 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Warning, + HealthCheckReason.IndexerDownloadClient, _localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage", new Dictionary { { "indexerNames", string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray()) } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs index f65f44962e5..0414ecfed74 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs @@ -45,12 +45,14 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.IndexerLongTermStatusAllUnavailable, _localizationService.GetLocalizedString("IndexerLongTermStatusAllUnavailableHealthCheckMessage"), "#indexers-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, + HealthCheckReason.IndexerLongTermStatusUnavailable, _localizationService.GetLocalizedString("IndexerLongTermStatusUnavailableHealthCheckMessage", new Dictionary { { "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs index d6f0ad90c9e..b92eb346a5a 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs @@ -36,6 +36,7 @@ public override HealthCheck Check() return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.IndexerNoDefinition, _localizationService.GetLocalizedString("IndexerNoDefinitionCheckHealthCheckMessage", new Dictionary { { "indexerNames", string.Join(", ", noDefinitionIndexers.Select(v => v.Definition.Name).ToArray()) } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs index 39aeac49ef1..565039fd7c8 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs @@ -38,12 +38,14 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.IndexerProxyStatusAllUnavailable, _localizationService.GetLocalizedString("IndexerProxyStatusAllUnavailableHealthCheckMessage"), "#indexer-proxies-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, + HealthCheckReason.IndexerProxyStatusUnavailable, _localizationService.GetLocalizedString("IndexerProxyStatusUnavailableHealthCheckMessage", new Dictionary { { "indexerProxyNames", string.Join(", ", badProxies.Select(v => v.Definition.Name)) } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs index 2e8846a75bf..bc078b26eb8 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs @@ -45,12 +45,14 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.IndexerStatusAllUnavailable, _localizationService.GetLocalizedString("IndexerStatusAllUnavailableHealthCheckMessage"), "#indexers-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, + HealthCheckReason.IndexerStatusUnavailable, _localizationService.GetLocalizedString("IndexerStatusUnavailableHealthCheckMessage", new Dictionary { { "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs index 2337403b8bf..94764b2c61d 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs @@ -51,6 +51,7 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Warning, + HealthCheckReason.IndexerVIPExpiring, _localizationService.GetLocalizedString("IndexerVipExpiringHealthCheckMessage", new Dictionary { { "indexerNames", string.Join(", ", expiringProviders.Select(v => v.Definition.Name).ToArray()) } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs index 0f3dffc1e9b..9f750dbdeb5 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs @@ -51,6 +51,7 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.IndexerVIPExpired, _localizationService.GetLocalizedString("IndexerVipExpiredHealthCheckMessage", new Dictionary { { "indexerNames", string.Join(", ", expiredProviders.Select(v => v.Definition.Name).ToArray()) } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs index daf5ee72500..7b28cccd856 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs @@ -40,12 +40,14 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.NotificationStatusAll, _localizationService.GetLocalizedString("NotificationStatusAllClientHealthCheckMessage"), "#notifications-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, + HealthCheckReason.NotificationStatusSingle, _localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage", new Dictionary { { "notificationNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/OutdatedDefinitionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/OutdatedDefinitionCheck.cs index c79d8dc71df..97809cf0187 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/OutdatedDefinitionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/OutdatedDefinitionCheck.cs @@ -36,13 +36,10 @@ public override HealthCheck Check() return new HealthCheck(GetType()); } - var healthType = HealthCheckResult.Warning; - var healthMessage = string.Format(_localizationService.GetLocalizedString("IndexerObsoleteCheckMessage"), - string.Join(", ", oldIndexers.Select(v => v.Definition.Name))); - return new HealthCheck(GetType(), - healthType, - healthMessage, + HealthCheckResult.Warning, + HealthCheckReason.OutdatedDefinitions, + string.Format(_localizationService.GetLocalizedString("IndexerObsoleteCheckMessage"), string.Join(", ", oldIndexers.Select(v => v.Definition.Name))), "#indexers-are-obsolete"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs index ba1e406578c..bc4cc004f2a 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs @@ -43,6 +43,7 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.ProxyResolveIp, _localizationService.GetLocalizedString("ProxyResolveIpHealthCheckMessage", new Dictionary { { "proxyHostName", _configService.ProxyHostname } @@ -65,6 +66,7 @@ public override HealthCheck Check() return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.ProxyBadRequest, _localizationService.GetLocalizedString("ProxyBadRequestHealthCheckMessage", new Dictionary { { "statusCode", response.StatusCode } @@ -78,6 +80,7 @@ public override HealthCheck Check() return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.ProxyFailed, _localizationService.GetLocalizedString("ProxyFailedToTestHealthCheckMessage", new Dictionary { { "url", request.Url } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ReleaseBranchCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ReleaseBranchCheck.cs index 6126471a550..b79a93d5c06 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ReleaseBranchCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ReleaseBranchCheck.cs @@ -19,11 +19,15 @@ public ReleaseBranchCheck(IConfigFileProvider configFileService, ILocalizationSe public override HealthCheck Check() { - var currentBranch = _configFileService.Branch.ToLower(); + var currentBranch = _configFileService.Branch.ToLowerInvariant(); - if (!Enum.GetNames(typeof(ReleaseBranches)).Any(x => x.ToLower() == currentBranch)) + if (Enum.GetNames(typeof(ReleaseBranches)).All(x => x.ToLowerInvariant() != currentBranch)) { - return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("ReleaseBranchCheckOfficialBranchMessage"), _configFileService.Branch), "#branch-is-not-a-valid-release-branch"); + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + HealthCheckReason.ReleaseBranch, + string.Format(_localizationService.GetLocalizedString("ReleaseBranchCheckOfficialBranchMessage"), _configFileService.Branch), + "#branch-is-not-a-valid-release-branch"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs index d2b3acf1338..86520a43a5b 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs @@ -43,7 +43,11 @@ public override HealthCheck Check() if (Math.Abs(result.DateTimeUtc.Subtract(systemTime).TotalDays) >= 1) { _logger.Error("System time mismatch. SystemTime: {0} Expected Time: {1}. Update system time", systemTime, result.DateTimeUtc); - return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("SystemTimeHealthCheckMessage"), "#system-time-off"); + return new HealthCheck(GetType(), + HealthCheckResult.Error, + HealthCheckReason.SystemTime, + _localizationService.GetLocalizedString("SystemTimeHealthCheckMessage"), + "#system-time-off"); } } catch (Exception e) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index 684d7f60ab2..b083309328b 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -48,6 +48,7 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.UpdateStartupTranslocation, _localizationService.GetLocalizedString( "UpdateStartupTranslocationHealthCheckMessage", new Dictionary @@ -61,6 +62,7 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.UpdateStartupNotWritable, _localizationService.GetLocalizedString( "UpdateStartupNotWritableHealthCheckMessage", new Dictionary @@ -75,6 +77,7 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), HealthCheckResult.Error, + HealthCheckReason.UpdateUiNotWritable, _localizationService.GetLocalizedString( "UpdateUiNotWritableHealthCheckMessage", new Dictionary @@ -94,6 +97,7 @@ public override HealthCheck Check() { return new HealthCheck(GetType(), BuildInfo.BuildDateTime.Before(DateTime.UtcNow.AddDays(-180)) ? HealthCheckResult.Error : HealthCheckResult.Warning, + HealthCheckReason.UpdateAvailable, _localizationService.GetLocalizedString("UpdateAvailableHealthCheckMessage", new Dictionary { { "version", $"v{latestAvailable.Version}" } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheck.cs b/src/NzbDrone.Core/HealthCheck/HealthCheck.cs index 1eba4c46f6b..32858dcab2c 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheck.cs @@ -11,6 +11,7 @@ public class HealthCheck : ModelBase public Type Source { get; set; } public HealthCheckResult Type { get; set; } + public HealthCheckReason Reason { get; set; } public string Message { get; set; } public HttpUri WikiUrl { get; set; } @@ -24,10 +25,11 @@ public HealthCheck(Type source) Type = HealthCheckResult.Ok; } - public HealthCheck(Type source, HealthCheckResult type, string message, string wikiFragment = null) + public HealthCheck(Type source, HealthCheckResult type, HealthCheckReason reason, string message, string wikiFragment = null) { Source = source; Type = type; + Reason = reason; Message = message; WikiUrl = MakeWikiUrl(wikiFragment ?? MakeWikiFragment(message)); } @@ -50,4 +52,41 @@ public enum HealthCheckResult Warning = 2, Error = 3 } + + public enum HealthCheckReason + { + AllowedHostsNotConfigured, + AppDataLocation, + ApplicationLongTermStatusAllUnavailable, + ApplicationLongTermStatusUnavailable, + ApplicationStatusAllClients, + ApplicationStatusSingleClient, + DownloadClientStatusAllClients, + DownloadClientStatusSingleClient, + IndexerCheckNoAvailableIndexers, + IndexerDownloadClient, + IndexerLongTermStatusAllUnavailable, + IndexerLongTermStatusUnavailable, + IndexerNoDefinition, + IndexerProxyStatusAllUnavailable, + IndexerProxyStatusUnavailable, + IndexerStatusAllUnavailable, + IndexerStatusUnavailable, + IndexerVIPExpired, + IndexerVIPExpiring, + MinimumApiKeyLength, + NotificationStatusAll, + NotificationStatusSingle, + OutdatedDefinitions, + ProxyBadRequest, + ProxyFailed, + ProxyResolveIp, + ReleaseBranch, + ServerNotification, + SystemTime, + UpdateAvailable, + UpdateStartupNotWritable, + UpdateStartupTranslocation, + UpdateUiNotWritable + } } diff --git a/src/NzbDrone.Core/HealthCheck/ServerSideNotificationService.cs b/src/NzbDrone.Core/HealthCheck/ServerSideNotificationService.cs index 51420e63bdc..7f02f95b7a0 100644 --- a/src/NzbDrone.Core/HealthCheck/ServerSideNotificationService.cs +++ b/src/NzbDrone.Core/HealthCheck/ServerSideNotificationService.cs @@ -61,7 +61,11 @@ private HealthCheck RetrieveServerChecks() var response = _client.Execute(request); var result = Json.Deserialize>(response.Content); - var checks = result.Select(x => new HealthCheck(GetType(), x.Type, x.Message, x.WikiUrl)).ToList(); + var checks = result.Select(x => new HealthCheck(GetType(), + x.Type, + HealthCheckReason.ServerNotification, + x.Message, + x.WikiUrl)).ToList(); // Only one health check is supported, services returns an ordered list, so use the first one return checks.FirstOrDefault() ?? new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs b/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs index 5107bf151d9..ddc4abd94e9 100644 --- a/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs +++ b/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs @@ -20,8 +20,8 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr { public class FlareSolverr : HttpIndexerProxyBase { - private readonly ICached _cache; private readonly IHttpProxySettingsProvider _proxySettingsProvider; + private readonly ICached _cache; public FlareSolverr(IHttpProxySettingsProvider proxySettingsProvider, IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager) : base(cloudRequestBuilder, httpClient, logger, localizationService) @@ -58,11 +58,16 @@ public override HttpResponse PostResponse(HttpResponse response) if (flaresolverrResponse.StatusCode != HttpStatusCode.OK && flaresolverrResponse.StatusCode != HttpStatusCode.InternalServerError) { - throw new FlareSolverrException("HTTP StatusCode not 200 or 500. Status is :" + response.StatusCode); + throw new FlareSolverrException("HTTP StatusCode not 200 or 500. Status is :" + flaresolverrResponse.StatusCode); } var result = JsonConvert.DeserializeObject(flaresolverrResponse.Content); + if (result?.Solution?.Cookies is not { Count: not 0 }) + { + throw new FlareSolverrException("Empty cookies returned by FlareSolverr."); + } + var newRequest = response.Request; //Cache the user-agent so we can inject it in next request to avoid re-solve @@ -194,7 +199,7 @@ public override ValidationResult Test() failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationBadRequest", new Dictionary { { "statusCode", response.StatusCode } }))); } - var result = JsonConvert.DeserializeObject(response.Content); + _ = JsonConvert.DeserializeObject(response.Content); } catch (Exception ex) { @@ -205,7 +210,7 @@ public override ValidationResult Test() return new ValidationResult(failures); } - private Uri GetProxyUri(HttpProxySettings proxySettings) + private static Uri GetProxyUri(HttpProxySettings proxySettings) { return proxySettings.Type switch { @@ -220,7 +225,7 @@ private class FlareSolverrRequest { public string Cmd { get; set; } public string Url { get; set; } - public Cookie[] Cookies { get; set; } + public IReadOnlyCollection Cookies { get; set; } public FlareSolverrProxy Proxy { get; set; } } @@ -270,7 +275,7 @@ private class Solution public string Status { get; set; } public Headers Headers { get; set; } public string Response { get; set; } - public Cookie[] Cookies { get; set; } + public IReadOnlyCollection Cookies { get; set; } public string UserAgent { get; set; } } @@ -288,7 +293,7 @@ private class Cookie public string SameSite { get; set; } public string ToHeaderValue() => $"{Name}={Value}"; - public System.Net.Cookie ToCookieObj() => new System.Net.Cookie(Name, Value); + public System.Net.Cookie ToCookieObj() => new(Name, Value); } private class Headers diff --git a/src/NzbDrone.Core/IndexerVersions/IndexerDefinitionUpdateService.cs b/src/NzbDrone.Core/IndexerVersions/IndexerDefinitionUpdateService.cs index abb40acac60..4833642a3d8 100644 --- a/src/NzbDrone.Core/IndexerVersions/IndexerDefinitionUpdateService.cs +++ b/src/NzbDrone.Core/IndexerVersions/IndexerDefinitionUpdateService.cs @@ -58,7 +58,6 @@ public class IndexerDefinitionUpdateService : IIndexerDefinitionUpdateService, I private readonly IHttpClient _httpClient; private readonly IAppFolderInfo _appFolderInfo; private readonly IDiskProvider _diskProvider; - private readonly IIndexerDefinitionVersionService _versionService; private readonly ICached _cache; private readonly Logger _logger; @@ -70,16 +69,15 @@ public class IndexerDefinitionUpdateService : IIndexerDefinitionUpdateService, I public IndexerDefinitionUpdateService(IHttpClient httpClient, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, - IIndexerDefinitionVersionService versionService, ICacheManager cacheManager, Logger logger) { _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; - _versionService = versionService; - _cache = cacheManager.GetCache(typeof(CardigannDefinition), "definitions"); _httpClient = httpClient; _logger = logger; + + _cache = cacheManager.GetCache(typeof(CardigannDefinition), "definitions"); } public List All() @@ -209,7 +207,7 @@ private CardigannDefinition GetUncachedDefinition(string fileKey) } } - var dbDefs = _versionService.All(); + var dbDefs = All(); //Check to ensure it's in versioned defs before we go to web if (dbDefs.Count > 0 && dbDefs.All(x => x.File != fileKey)) diff --git a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetRequestGenerator.cs index d9c8e9e5b75..23225df5f40 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetRequestGenerator.cs @@ -81,7 +81,9 @@ public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCrit else if (searchCriteria.Season > 0 && int.TryParse(searchCriteria.Episode, out var episode) && episode > 0) { // Standard (S/E) Episode - parameters.Name = $"S{searchCriteria.Season:00}E{episode:00}%"; + parameters.Name = episode is > 0 and < 100 + ? $"S{searchCriteria.Season:00}E%{episode:00}%" + : $"S{searchCriteria.Season:00}E{episode:00}%"; parameters.Category = "Episode"; pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset)); } diff --git a/src/NzbDrone.Core/Indexers/Definitions/FunFile.cs b/src/NzbDrone.Core/Indexers/Definitions/FunFile.cs index 91646cb37b2..dd5b21501fb 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/FunFile.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/FunFile.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Indexers.Definitions; +[Obsolete("Moved to YML for Cardigann")] public class FunFile : TorrentIndexerBase { public override string Name => "FunFile"; diff --git a/src/NzbDrone.Core/Indexers/Definitions/Headphones/Headphones.cs b/src/NzbDrone.Core/Indexers/Definitions/Headphones/Headphones.cs index f8028c0b856..28c7a35797a 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Headphones/Headphones.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Headphones/Headphones.cs @@ -67,10 +67,10 @@ public override async Task Download(Uri link) downloadBytes = response.ResponseData; elapsedTime = response.ElapsedTime; } - catch (Exception) + catch (Exception ex) { _indexerStatusService.RecordFailure(Definition.Id); - _logger.Error("Download failed"); + _logger.Error(ex, "Release download failed ({0})", link.AbsoluteUri); throw; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/MTeamTp.cs b/src/NzbDrone.Core/Indexers/Definitions/MTeamTp.cs index 6af9468e167..dc911296ed2 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/MTeamTp.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/MTeamTp.cs @@ -139,6 +139,7 @@ private static IndexerCapabilities SetCapabilities() caps.Categories.AddCategoryMapping(423, NewznabStandardCategory.PCGames, "PCGame(PC遊戲)"); caps.Categories.AddCategoryMapping(448, NewznabStandardCategory.ConsoleOther, "TvGame(TV遊戲)"); caps.Categories.AddCategoryMapping(405, NewznabStandardCategory.TVAnime, "Anime(動畫)"); + caps.Categories.AddCategoryMapping(453, NewznabStandardCategory.TVAnime, "Anime(動畫)/BluRay"); caps.Categories.AddCategoryMapping(407, NewznabStandardCategory.TVSport, "Sports(運動)"); caps.Categories.AddCategoryMapping(427, NewznabStandardCategory.BooksEBook, "Study/Edu ebook(教育書面)"); caps.Categories.AddCategoryMapping(422, NewznabStandardCategory.PC0day, "Software(軟體)"); diff --git a/src/NzbDrone.Core/Indexers/Definitions/MyAnonamouse.cs b/src/NzbDrone.Core/Indexers/Definitions/MyAnonamouse.cs index 1aa0dd7c02a..ae94a52f5e9 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/MyAnonamouse.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/MyAnonamouse.cs @@ -230,8 +230,8 @@ private IEnumerable GetPagedRequests(SearchCriteriaBase searchCr { "tor[srchIn][narrator]", "true" }, { "tor[searchIn]", "torrents" }, { "tor[sortType]", "default" }, - { "tor[perpage]", searchCriteria.Limit?.ToString() ?? "100" }, - { "tor[startNumber]", searchCriteria.Offset?.ToString() ?? "0" }, + { "tor[startNumber]", searchCriteria.Offset?.ToString(CultureInfo.InvariantCulture) ?? "0" }, + { "perpage", searchCriteria.Limit?.ToString(CultureInfo.InvariantCulture) ?? "100" }, { "thumbnails", "1" }, // gives links for thumbnail sized versions of their posters { "description", "1" } // include the description }; diff --git a/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs index b4164022525..424cef4600e 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs @@ -92,7 +92,6 @@ public override IEnumerable DefaultDefinitions get { yield return GetDefinition("AnimeTosho", "Anime NZB/DDL mirror", settings: GetSettings("https://feed.animetosho.org"), categories: new[] { 2020, 5070 }); - yield return GetDefinition("MoreThanTV", "Private torrent tracker for TV / MOVIES", settings: GetSettings("https://www.morethantv.me", apiPath: @"/api/torznab"), categories: new[] { 2000, 5000 }); yield return GetDefinition("Torrent Network", "Torrent Network (TN) is a GERMAN Private site for TV / MOVIES / GENERAL", language: "de-DE", settings: GetSettings("https://tntracker.org", apiPath: @"/api/torznab/api")); yield return GetDefinition("Generic Torznab", "A Newznab-like api for torrents.", settings: GetSettings("")); } diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index 86f125e45de..a444530220e 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -284,13 +284,20 @@ or HttpStatusCode.TemporaryRedirect response = await _httpClient.ExecuteProxiedAsync(request, Definition); } - while (response.StatusCode is HttpStatusCode.MovedPermanently or HttpStatusCode.Found or HttpStatusCode.SeeOther); + while (response.StatusCode is HttpStatusCode.MovedPermanently or HttpStatusCode.Found or HttpStatusCode.SeeOther or HttpStatusCode.TemporaryRedirect or HttpStatusCode.PermanentRedirect); } fileData = response.ResponseData; elapsedTime = response.ElapsedTime; _logger.Debug("Downloaded for release finished ({0} bytes from {1})", fileData.Length, link.AbsoluteUri); + + if (response.Headers?.ContentType?.Contains("text/html", StringComparison.OrdinalIgnoreCase) == true) + { + throw new HttpException(request, response, "Site responded with html content."); + } + + ValidateDownloadData(fileData); } catch (HttpException ex) { @@ -317,15 +324,13 @@ or HttpStatusCode.TemporaryRedirect throw new ReleaseDownloadException("Download failed", ex); } - catch (Exception) + catch (Exception ex) { _indexerStatusService.RecordFailure(Definition.Id); - _logger.Error("Download failed"); + _logger.Error(ex, "Release download failed ({0})", link.AbsoluteUri); throw; } - ValidateDownloadData(fileData); - return new IndexerDownloadResponse(fileData, elapsedTime); } diff --git a/src/NzbDrone.Core/Indexers/TorrentRssParser.cs b/src/NzbDrone.Core/Indexers/TorrentRssParser.cs index eb78a811176..29c0407f816 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRssParser.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRssParser.cs @@ -88,7 +88,7 @@ protected virtual string GetInfoHash(XElement item) { try { - return MagnetLink.Parse(magnetUrl).InfoHash.ToHex(); + return MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex(); } catch { diff --git a/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs b/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs index 535df3d9f67..723bc12249f 100644 --- a/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs +++ b/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs @@ -2,6 +2,7 @@ namespace NzbDrone.Core.Lifecycle { + [LifecycleEvent] public class ApplicationShutdownRequested : IEvent { public bool Restarting { get; } diff --git a/src/NzbDrone.Core/Lifecycle/ApplicationStartedEvent.cs b/src/NzbDrone.Core/Lifecycle/ApplicationStartedEvent.cs index 8e0d45e1556..f6e458c1fa6 100644 --- a/src/NzbDrone.Core/Lifecycle/ApplicationStartedEvent.cs +++ b/src/NzbDrone.Core/Lifecycle/ApplicationStartedEvent.cs @@ -2,6 +2,7 @@ namespace NzbDrone.Core.Lifecycle { + [LifecycleEvent] public class ApplicationStartedEvent : IEvent { } diff --git a/src/NzbDrone.Core/Lifecycle/ApplicationStartingEvent.cs b/src/NzbDrone.Core/Lifecycle/ApplicationStartingEvent.cs index 464ff5ff42e..eb54ea5ee85 100644 --- a/src/NzbDrone.Core/Lifecycle/ApplicationStartingEvent.cs +++ b/src/NzbDrone.Core/Lifecycle/ApplicationStartingEvent.cs @@ -2,6 +2,7 @@ namespace NzbDrone.Core.Lifecycle { + [LifecycleEvent] public class ApplicationStartingEvent : IEvent { } diff --git a/src/NzbDrone.Core/Lifecycle/LifecycleService.cs b/src/NzbDrone.Core/Lifecycle/LifecycleService.cs index 9ed36a42e97..b3be3bec1dd 100644 --- a/src/NzbDrone.Core/Lifecycle/LifecycleService.cs +++ b/src/NzbDrone.Core/Lifecycle/LifecycleService.cs @@ -1,10 +1,8 @@ -using NLog; -using NzbDrone.Common; +using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Lifecycle.Commands; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; -using IServiceProvider = NzbDrone.Common.IServiceProvider; namespace NzbDrone.Core.Lifecycle { @@ -18,41 +16,28 @@ public class LifecycleService : ILifecycleService, IExecute, IE { private readonly IEventAggregator _eventAggregator; private readonly IRuntimeInfo _runtimeInfo; - private readonly IServiceProvider _serviceProvider; private readonly Logger _logger; public LifecycleService(IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, - IServiceProvider serviceProvider, Logger logger) { _eventAggregator = eventAggregator; _runtimeInfo = runtimeInfo; - _serviceProvider = serviceProvider; _logger = logger; } public void Shutdown() { _logger.Info("Shutdown requested."); - _eventAggregator.PublishEvent(new ApplicationShutdownRequested()); - - if (_runtimeInfo.IsWindowsService) - { - _serviceProvider.Stop(ServiceProvider.SERVICE_NAME); - } + _eventAggregator.PublishEvent(new ApplicationShutdownRequested(false)); } public void Restart() { _logger.Info("Restart requested."); - + _runtimeInfo.RestartPending = true; _eventAggregator.PublishEvent(new ApplicationShutdownRequested(true)); - - if (_runtimeInfo.IsWindowsService) - { - _serviceProvider.Restart(ServiceProvider.SERVICE_NAME); - } } public void Execute(ShutdownCommand message) diff --git a/src/NzbDrone.Core/Localization/Core/bg.json b/src/NzbDrone.Core/Localization/Core/bg.json index 5902be93514..8eb2b0e34ba 100644 --- a/src/NzbDrone.Core/Localization/Core/bg.json +++ b/src/NzbDrone.Core/Localization/Core/bg.json @@ -807,5 +807,9 @@ "WouldYouLikeToRestoreBackup": "Искате ли да възстановите архива '{name}'?", "XmlRpcPath": "XML RPC път", "days": "дни", - "Category": "Категория" + "Category": "Категория", + "ApplicationAuthPassword": "Auth парола", + "ApplicationAuthPasswordHelpText": "HTTP Basic аутентикационна парола използвана от {appName} когато достъпва това приложение. Задайте ако приложението стои зад Basic аутентикационно ривърс прокси.", + "ApplicationAuthUsername": "Auth потребителско име", + "ApplicationAuthUsernameHelpText": "HTTP Basic аутентикационно потребителско име използвано от {appName} докато достъпвате това приложение. Задайте ако приложението стои зад Basic аутентикационно ривърс прокси" } diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 468b27b01b9..a694cedc885 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -708,7 +708,7 @@ "IndexerHDBitsSettingsOrigins": "Orígens", "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "Agent d'usuari associat a la cookie utilitzada des del navegador", "IndexerNewznabSettingsApiKeyHelpText": "Clau de l'API del lloc", - "IndexerNzbIndexSettingsApiKeyHelpText": "Clau de l'API del lloc", + "IndexerNzbIndexSettingsApiKeyHelpText": "Clau API del lloc (opcional: la cerca funciona sense clau, però amb un límit de peticions més baix)", "IndexerSettingsAppsMinimumSeedersHelpText": "«Mínims filtradors requerits per les Aplicacions perquè l'indexador s'agafi", "IndexerSettingsFreeleechOnly": "Només Freeleech", "IndexerSettingsGrabLimit": "Límit de captura", @@ -721,7 +721,7 @@ "DeleteAppProfile": "Suprimeix el perfil de l'aplicació", "TVSearchTypes": "Tipus de cerca de TV", "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Cerca només els llançaments de freeleech", - "IndexerMTeamTpSettingsApiKeyHelpText": "Clau API del Lloc (trobada a Tauler de control de l'usuari => Seguretat => Laboratori)", + "IndexerMTeamTpSettingsApiKeyHelpText": "Clau API del lloc (la trobaràs a User Control Panel => Laboratory)", "MinimumSeedersHelpText": "Visors mínims requerits per l'aplicació perquè l'indexador s'agafi", "NoIndexerHistory": "No s'ha trobat cap historial per a aquest indexador", "SearchQueries": "Cerca consultes", @@ -811,5 +811,12 @@ "ApplicationAuthPassword": "Contrasenya d'autenticació", "ApplicationAuthPasswordHelpText": "Contrasenya d'autenticació bàsica HTTP utilitzada per {appName} en accedir a aquesta aplicació. Establiu-la si l'aplicació es troba darrere d'un proxy invers protegit amb autenticació bàsica.", "ApplicationAuthUsername": "Nom d'usuari d'autenticació", - "ApplicationAuthUsernameHelpText": "Nom d'usuari d'autenticació bàsica HTTP utilitzada per {appName} en accedir a aquesta aplicació. Definiu-ho si l'aplicació es troba darrere d'un proxy invers protegit amb autenticació bàsica." + "ApplicationAuthUsernameHelpText": "Nom d'usuari d'autenticació bàsica HTTP utilitzada per {appName} en accedir a aquesta aplicació. Definiu-ho si l'aplicació es troba darrere d'un proxy invers protegit amb autenticació bàsica.", + "AllowedHosts": "Hostes permesos", + "AllowedHostsHelpText": "Llista de noms d'hostes i adreces IP separades per comes que {appName} n'acceptarà sol·licituds. Utilitza '*.' com a comodí per subdominis. Deixa-ho buit per permetre tots els hostes.", + "AllowedHostsNotConfiguredHealthCheckMessage": "Hostes permesos no configurats, {appName} acceptarà sol·licituds de qualsevol nom d'hoste. Configura els hostes permesos a les configuracions Generals per restringir els hostes que poden contactar {appName}.", + "IndexerIPTorrentsSettingsSearchSortBy": "Ordena la cerca per", + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Selecciona l'ordre en què s'han d'ordenar els resultats de la cerca", + "TrustedNetworks": "Xarxes de confiança", + "TrustedNetworksHelpText": "Llista separada per comes d'adreces IP o de xarxes en notació CIDR on hi ha els proxies inversos de confiança. Per exemple: 172.17.0.1, 10.0.0.0/8 o fc00::/7. Afegeix-hi només proxies configurats correctament per enviar les capçaleres adequades." } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 0deac6fc443..0c26104a7d3 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -38,23 +38,23 @@ "AudioSearch": "Audio Suche", "Auth": "Authentifizierung", "Authentication": "Authentifizierung", - "AuthenticationMethodHelpText": "Nutzername und Passwort benötig, um auf {appName} zuzugreifen", + "AuthenticationMethodHelpText": "Mit Nutzername und Passwort authentifizieren, um auf {appName} zuzugreifen", "Automatic": "Automatisch", "AutomaticSearch": "Automatische Suche", "Backup": "Backup", "BackupFolderHelpText": "Relative Pfade befinden sich im AppData-Verzeichnis von {appName}", "BackupIntervalHelpText": "Dauer zwischen automatischen Backups", - "BackupNow": "Jetzt sichern", + "BackupNow": "Backup erstellen", "BackupRetentionHelpText": "Automatische Backups, die älter als die Aufbewahrungsfrist sind, werden automatisch bereinigt", "Backups": "Backups", - "BeforeUpdate": "Vor Aktualisierung", + "BeforeUpdate": "Vor Update", "BindAddress": "Adresse binden", "BindAddressHelpText": "Gültige IP-Adresse, localhost oder \"*“ für alle Schnittstellen", "BookSearch": "Buch Suche", "BookSearchTypes": "Buch-Suchtypen", "Branch": "Branch", - "BranchUpdate": "Branch, um {appName} zu aktualisieren", - "BranchUpdateMechanism": "Git-Branch für den externen Updatemechanismus", + "BranchUpdate": "Zu verwendender Branch, um {appName} zu updaten", + "BranchUpdateMechanism": "Git-Branch für den externen Update-Mechanismus", "BypassProxyForLocalAddresses": "Proxy für lokale Adressen umgehen", "Cancel": "Abbrechen", "CancelPendingTask": "Möchten Sie diese ausstehende Aufgabe wirklich abbrechen?", @@ -95,9 +95,9 @@ "DeleteIndexerProxy": "Indexer Proxy löschen", "DeleteIndexerProxyMessageText": "Bist du sicher, dass du den Indexer-Proxy „{name}“ löschen möchtest?", "DeleteNotification": "Benachrichtigung löschen", - "DeleteNotificationMessageText": "Sind Sie sicher, dass Sie die Benachrichtigung '{name}' löschen wollen?", + "DeleteNotificationMessageText": "Sind Sie sicher, dass Sie die Benachrichtigung \"{name}\" löschen wollen?", "DeleteTag": "Tag löschen", - "DeleteTagMessageText": "Sind Sie sicher, dass Sie das Tag '{label}' löschen wollen?", + "DeleteTagMessageText": "Sind Sie sicher, dass Sie das Tag \"{label}\" löschen wollen?", "Description": "Beschreibung", "Details": "Details", "DevelopmentSettings": "Entwicklungseinstellungen", @@ -460,7 +460,7 @@ "ApplicationUrlHelpText": "Die externe URL dieser Anwendung, einschließlich http(s)://, Port und URL-Basis", "ApplyChanges": "Änderungen anwenden", "CountIndexersSelected": "{count} Indexer ausgewählt", - "DeleteSelectedDownloadClients": "Lösche Downladmanager", + "DeleteSelectedDownloadClients": "Downlad-Client(s) löschen", "DeleteSelectedApplicationsMessageText": "Bist du sicher, dass du {count} ausgewählte Anwendung(en) löschen möchtest?", "DeleteSelectedDownloadClientsMessageText": "Sind Sie sicher, dass Sie {count} ausgewählte Download-Client(s) löschen wollen?", "DeleteSelectedIndexersMessageText": "Sind Sie sicher, dass Sie {count} ausgewählte(n) Indexer löschen möchten?", @@ -491,7 +491,7 @@ "Artist": "Interpret", "Author": "Autor", "Book": "Buch", - "ConnectionLostReconnect": "{appName} wird versuchen, automatisch eine Verbindung herzustellen, oder Sie können unten auf „Neu laden“ klicken.", + "ConnectionLostReconnect": "{appName} wird automatisch versuchen, eine Verbindung herzustellen, oder Sie können unten auf \"Neu laden\" klicken.", "ConnectionLostToBackend": "{appName} hat die Verbindung zum Backend verloren und muss neu geladen werden, um die Funktionalität wiederherzustellen.", "RecentChanges": "Kürzliche Änderungen", "WhatsNew": "Was ist neu?", @@ -525,15 +525,15 @@ "AddDownloadClientImplementation": "Download-Client hinzufügen - {implementationName}", "AddIndexerImplementation": "Indexer hinzufügen - {implementationName}", "AddIndexerProxyImplementation": "Indexer Proxy hinzufügen - {implementationName}", - "AppUpdatedVersion": "{appName} wurde auf die Version `{version}` aktualisiert. Um die neusten Funktionen zu bekommen lade {appName} neu", - "AuthenticationRequiredWarning": "Um unberechtigte Fernzugriffe zu verhindern, benötigt {appName} jetzt eingeschaltete Authentifizierung. Du kannst die Authentifizierung optional für lokale Adressen ausschalten.", + "AppUpdatedVersion": "{appName} wurde auf die Version \"{version}\" geupdatet. Um die neuesten Funktionen zu erhalten, laden Sie {appName} neu", + "AuthenticationRequiredWarning": "Um unberechtigte Fernzugriffe zu verhindern, benötigt {appName} ab sofort eine aktivierte Authentifizierung. Sie können die Authentifizierung optional für lokale Adressen deaktivieren.", "AuthenticationRequired": "Authentifizierung benötigt", "AuthenticationRequiredHelpText": "Einstellen, welche Anfragen eine Authentifizierung erfordern. Nicht ändern, außer Sie verstehen die Risiken.", "AuthenticationRequiredUsernameHelpTextWarning": "Neuen Benutzernamen eingeben", "AuthenticationMethodHelpTextWarning": "Bitte wählen Sie eine gültige Authentifizierungsmethode aus", "AuthenticationRequiredPasswordHelpTextWarning": "Neues Passwort eingeben", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Neues Passwort bestätigen", - "DefaultNameCopiedProfile": "{name} – Kopie", + "DefaultNameCopiedProfile": "{name} - Kopie", "AuthenticationMethod": "Authentifizierungsmethode", "Clone": "Klonen", "CountDownloadClientsSelected": "{count} Download-Client(s) ausgewählt", @@ -546,7 +546,7 @@ "CountApplicationsSelected": "{count} Ausgewählte Sammlung(en)", "DownloadClientAriaSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads. Lassen Sie das Feld leer, um den standardmäßigen Aria2-Speicherort zu verwenden", "ManageClients": "Clients verwalten", - "BlackholeFolderHelpText": "Ordner, in dem {appName} die Datei {extension} speichert", + "BlackholeFolderHelpText": "Ordner, in dem {appName} die {extension} Datei speichert", "DownloadClientSettingsInitialStateHelpText": "Ausgangszustand für zu {clientName} hinzugefügte Torrents", "DownloadClientSettingsDestinationHelpText": "Legt das Ziel für den Download manuell fest, leer lassen, um die Standardeinstellung zu verwenden", "DownloadClientSettingsUrlBaseHelpText": "Fügt ein Präfix zur {clientName} URL hinzu, z. B. {url}", @@ -556,12 +556,12 @@ "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“)", + "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, ist standardmäßig \"{port}\"", "DownloadClientNzbgetSettingsAddPausedHelpText": "Diese Option erfordert mindestens NzbGet Version 16.0", "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Erste und letzte Teile zuerst herunterladen (qBittorrent 4.1.0+)", "IndexerBeyondHDSettingsSearchTypes": "Suchtyp", - "DownloadClientFloodSettingsUrlBaseHelpText": "Fügt der Flood-API ein Präfix hinzu, z. B. {url}", + "DownloadClientFloodSettingsUrlBaseHelpText": "Fügt der Flood-API ein Präfix hinzu, z. B. {url}", "DownloadClientFreeboxSettingsApiUrl": "API-URL", "DownloadClientFreeboxSettingsApiUrlHelpText": "Freebox-API Basis-URL mit API-Version definieren, z. B. \"{url}\", standardmäßig \"{defaultApiUrl}\"", "DownloadClientRTorrentSettingsUrlPathHelpText": "Pfad zum XMLRPC-Endpunkt, siehe {url}. Dies ist normalerweise RPC2 oder [Pfad zu ruTorrent]{url2}, wenn ruTorrent verwendet wird.", @@ -577,7 +577,7 @@ "DownloadClientFloodSettingsAdditionalTags": "Zusätzliche Tags", "DownloadClientFloodSettingsTagsHelpText": "Initiale Tags eines Downloads. Um korrekt erkannt zu werden, muss ein Download alle Initial-Tags haben. Dies vermeidet Konflikte mit anderweitigen Downloads.", "DownloadClientFreeboxSettingsAppToken": "App-Token", - "DownloadClientFreeboxSettingsAppTokenHelpText": "App-Token, das beim Erstellen des Zugriffs auf die Freebox-API abgerufen wird (z. B. „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", @@ -623,7 +623,7 @@ "UpdateAppDirectlyLoadError": "{appName} kann nicht direkt aktualisiert werden,", "UpdaterLogFiles": "Updater Logdateien", "WouldYouLikeToRestoreBackup": "Wollen Sie das Backup \"{name}\" wiederherstellen?", - "AptUpdater": "Verwenden Sie apt, um die Aktualisierung zu installieren", + "AptUpdater": "Verwenden Sie apt, um das Update zu installieren", "InstallLatest": "Neueste Version installieren", "CurrentlyInstalled": "Derzeit installiert", "Mixed": "Gemischt", @@ -808,5 +808,11 @@ "AverageResponseTimesMs": "Durchschnittliche Indexer-Antwortzeiten (ms)", "BasicSearch": "Einfache Suche", "DownloadClientUTorrentProviderMessage": "uTorrent ist dafür bekannt, Kryptominer, Malware und Werbung zu enthalten. Wir empfehlen dringend, einen anderen Client zu verwenden.", - "ApplicationAuthPassword": "Passwort" + "ApplicationAuthPassword": "Passwort", + "AllowedHosts": "Erlaubte Hosts", + "AllowedHostsHelpText": "Kommagetrennte Liste von Hostnamen und IP Adressen, von denen {appName} Anfragen akzeptiert. Verwende \"*\" als Wildcard für Subdomänen. Leer lassen, um alle Hosts zu erlauben.", + "AllowedHostsNotConfiguredHealthCheckMessage": "Erlaubte Hosts sind nicht konfiguriert, {appName} wird Anfragen von jedem beliebigen Host akzeptieren. Setze Erlaubte Hosts unter allgemeine Einstellungen, um festzulegen, welche Hostnamen verwendet werden können, um {appName} zu erreichen.", + "TrustedNetworks": "Vertrauenswürdige Netzwerke", + "TrustedNetworksHelpText": "Kommagetrenne Liste von IP Adressen oder Netzwerken in CIDR Notation, auf denen vertrauenswürdige Reverse Proxys laufen, z.B. 172.17.0.1, 10.0.0.0/8 oder fc00::/7. Fügen Sie nur Proxys hinzu, die so konfiguriert sind, dass sie die korrekten Header senden.", + "ApplicationAuthUsername": "Benutzername" } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b1bbd2c8be5..1e8af6c91c2 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -32,6 +32,9 @@ "All": "All", "AllIndexersHiddenDueToFilter": "All indexers are hidden due to applied filter.", "AllSearchResultsHiddenByFilter": "All search results are hidden by the applied filter.", + "AllowedHosts": "Allowed Hosts", + "AllowedHostsHelpText": "Comma separated list of hostnames and IP addresses that {appName} should be accessible as, including system's hostname or docker container name. Leave empty to allow all hosts. For example: prowlarr, prowlarr.example.com, 192.168.0.100", + "AllowedHostsNotConfiguredHealthCheckMessage": "Allowed Hosts is not configured, {appName} will accept requests for any hostname or IP address. Set Allowed Hosts in General settings to restrict which hostnames can be used to reach {appName}.", "Analytics": "Analytics", "AnalyticsEnabledHelpText": "Send anonymous usage and error information to {appName}'s servers. This includes information on your browser, which {appName} WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes.", "Any": "Any", @@ -381,8 +384,8 @@ "IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent", "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "User-Agent associated with cookie used from Browser", "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Search freeleech releases only", - "IndexerIPTorrentsSettingsSearchSortBy": "Search Sort By", - "IndexerIPTorrentsSettingsSearchSortByHelpText": "Select the order in which to sort search results", + "IndexerIPTorrentsSettingsSearchSortBy": "Search Sort By", + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Select the order in which to sort search results", "IndexerId": "Indexer ID", "IndexerInfo": "Indexer Info", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "All indexers are unavailable due to failures for more than 6 hours", @@ -754,6 +757,8 @@ "TotalUserAgentGrabs": "Total User Agent Grabs", "TotalUserAgentQueries": "Total User Agent Queries", "Track": "Track", + "TrustedNetworks": "Trusted Networks", + "TrustedNetworksHelpText": "Comma separated list of IP addresses or networks in CIDR notation that trusted reverse proxies are on. e.g. 172.17.0.1, 10.0.0.0/8 or fc00::/7. Only add proxies that are properly configured to send the correct headers.", "TvSearch": "TV Search", "Type": "Type", "UI": "UI", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index aeb1a25b0a8..edac35fb62c 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -813,5 +813,10 @@ "ApplicationAuthUsername": "Usuario de autenticación", "ApplicationAuthUsernameHelpText": "Usuario de autenticación básico en HTTP usado por {appName} cuando se accede a esta aplicación. Establecer si la aplicación se sitúa tras un proxy inverso protegido por autenticación básica.", "IndexerIPTorrentsSettingsSearchSortByHelpText": "Selecciona el orden en que se ordenan los resultados de búsqueda", - "IndexerIPTorrentsSettingsSearchSortBy": "Orden de búsqueda" + "IndexerIPTorrentsSettingsSearchSortBy": "Orden de búsqueda", + "AllowedHosts": "Hosts permitidos", + "AllowedHostsHelpText": "Lista separada por comas de nombres de host y direcciones IP para la que {appName} aceptará peticiones. Usa '*.' como un wildcard para subdominios. Dejar vacío para permitir todos los hosts.", + "AllowedHostsNotConfiguredHealthCheckMessage": "Hosts permitidos no está configurado, {appName} aceptará peticiones para cualquier nombre de host. Establece Hosts permitidos en Ajustes generales para restringir qué nombres de host pueden ser usados para alcanzar {appName}.", + "TrustedNetworks": "Redes de confianza", + "TrustedNetworksHelpText": "Lista separada por comas de direcciones IP o redes en notación CIDR en las que están los proxies inversos de confianza. P. ej. 172.17.0.1, 10.0.0.0/8 o fc00::/7. Añadir solo proxies que están configurados adecuadamente para enviar las cabeceras correctas." } diff --git a/src/NzbDrone.Core/Localization/Core/es_MX.json b/src/NzbDrone.Core/Localization/Core/es_MX.json index 0967ef424bc..1e7d343b40d 100644 --- a/src/NzbDrone.Core/Localization/Core/es_MX.json +++ b/src/NzbDrone.Core/Localization/Core/es_MX.json @@ -1 +1,5 @@ -{} +{ + "About": "hola", + "Actions": "Acciones", + "Add": "Añade" +} diff --git a/src/NzbDrone.Core/Localization/Core/et.json b/src/NzbDrone.Core/Localization/Core/et.json index 0967ef424bc..28185bc8e52 100644 --- a/src/NzbDrone.Core/Localization/Core/et.json +++ b/src/NzbDrone.Core/Localization/Core/et.json @@ -1 +1,736 @@ -{} +{ + "About": "Teave", + "Actions": "Toimingud", + "AcceptConfirmationModal": "Nõustu kinnitusaknaga", + "Add": "Lisa", + "AddApplication": "Lisa rakendus", + "AddApplicationImplementation": "Lisa rakendus - {implementationName}", + "ActiveApps": "Aktiivsed rakendused", + "ActiveIndexers": "Aktiivsed indekserid", + "AddIndexer": "Lisa indekser", + "AddIndexerImplementation": "Lisa indekser - {implementationName}", + "AddIndexerProxy": "Lisa indekseri proksi", + "AddIndexerProxyImplementation": "Lisa indekseri proksi - {implementationName}", + "AddNewIndexer": "Lisa uus indekser", + "AllIndexersHiddenDueToFilter": "Kõik indekserid on rakendatud filtri tõttu peidetud.", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Kui torrent on räsi järgi blokeeritud, ei pruugi mõned indekserid seda RSS-i või otsingu ajal õigesti tagasi lükata. Selle funktsiooni lubamine võimaldab torrenti tagasi lükata pärast selle haaramist, kuid enne allalaadijasse saatmist.", + "ApplicationTagsHelpText": "Sünkrooni sellesse rakendusse indekserid, millel on vähemalt üks kattuv silt. Kui siin silte kirjas pole, ei takistata ühegi indekseri sünkroonimist nende siltide tõttu.", + "ApplicationTagsHelpTextWarning": "Silte tuleks kasutada ettevaatlikult, sest neil võivad olla ettenägematud tagajärjed. Sildiga rakendus sünkroonib ainult nende indekseritega, millel on sama silt.", + "ApplyTagsHelpTextHowToApplyIndexers": "Kuidas rakendada silte valitud indekseritele", + "AppsMinimumSeedersHelpText": "Väikseim külvajate (seederite) arv, mis on rakendustele vajalik sellest indekserist haaramiseks. Tühja välja korral kasutatakse sünkroonimisprofiili vaikeväärtust", + "AreYouSureYouWantToDeleteIndexer": "Kas soovid kindlasti kustutada rakendusest {appName} indekseri „{name}“?", + "AverageResponseTimesMs": "Indekserite keskmised vastuseajad (ms)", + "CountIndexersAvailable": "Saadaolevaid indeksereid: {count}", + "CountIndexersSelected": "Valitud indekserid: {count}", + "DeleteIndexerProxy": "Kustuta indekseri proksi", + "DeleteIndexerProxyMessageText": "Kas soovid kindlasti kustutada indekseri proksi '{name}'?", + "DeleteSelectedIndexer": "Kustuta valitud indekser", + "DeleteSelectedIndexers": "Kustuta valitud indekserid", + "DeleteSelectedIndexersMessageText": "Kas soovid kindlasti kustutada {count} valitud indekseri?", + "EditIndexer": "Muuda indekserit", + "EditIndexerImplementation": "Muuda indekserit - {implementationName}", + "EditIndexerProxyImplementation": "Muuda indekseri proksit – {implementationName}", + "EditSelectedIndexers": "Muuda valitud indeksereid", + "EnableIndexer": "Luba indekser", + "EnableRssHelpText": "Luba indekseri RSS-voog", + "FilterPlaceHolder": "Otsi indeksereid", + "Indexer": "Indekser", + "IndexerAlphaRatioSettingsExcludeScene": "Välista SCENE", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Välista tulemustest SCENE-väljalasked", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Otsi ainult freeleech-väljalaskeid", + "IndexerAlreadySetup": "Vähemalt üks indekser on juba seadistatud", + "IndexerAuth": "Indekseri autentimine", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Otsi ainult freeleech-väljalaskeid", + "IndexerAvistazSettingsPasswordHelpText": "Lehe parool", + "IndexerAvistazSettingsPidHelpText": "PID \"Minu konto\" või \"Minu profiili\" lehelt", + "IndexerAvistazSettingsUsernameHelpText": "Lehe kasutajanimi", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Sellel indekseril on API kasutamine lubatud vaid alates liikme staatusest.", + "IndexerBeyondHDSettingsApiKeyHelpText": "Lehe API-võti (asub: Minu turvalisus => API-võti)", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Otsi ainult freeleech-väljalaskeid", + "IndexerBeyondHDSettingsLimitedOnly": "Vaid piiratud", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Otsi ainult freeleech-väljalaskeid (piiratud üleslaadimine)", + "IndexerBeyondHDSettingsRefundOnly": "Ainult Refund", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Otsi ainult Refund-väljalaskeid", + "IndexerBeyondHDSettingsRewindOnly": "Ainult Rewind", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Otsi ainult Rewind-väljalaskeid", + "IndexerBeyondHDSettingsRssKeyHelpText": "Veebilehe RSS-võti (leiad: Minu turvalisus => RSS-võti)", + "IndexerBeyondHDSettingsSearchTypes": "Otsitavad tüübid", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Vali soovitud väljalasete tüübid. Kui valik puudub, rakendatakse kõiki.", + "IndexerCategories": "Indekseri kategooriad", + "IndexerDetails": "Indekseri üksikasjad", + "IndexerDisabled": "Indekseerija on keelatud", + "IndexerDownloadClientHealthCheckMessage": "Vigaste allalaadimisklientidega indekserid: {indexerNames}.", + "IndexerDownloadClientHelpText": "Määra allalaadimisklient, mida rakendus {appName} kasutab sellest indekseerijast allalaadimiste jaoks", + "IndexerFailureRate": "Indekseri tõrgete määr", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Otsi ainult freeleech-väljalaskeid", + "IndexerFileListSettingsPasskeyHelpText": "Saidi pääsuvõti (see on sinu allalaadimiskliendis nähtava jälitaja URL-i tähe- ja numbrikombinatsioon)", + "IndexerFileListSettingsUsernameHelpText": "Lehe kasutajanimi", + "IndexerFlags": "Indekseri lipud", + "IndexerGazelleGamesSettingsApiKeyHelpText": "Veebilehe API-võti (leiad: Seaded => Juurdepääsuseaded)", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Nõutud on User (kasutaja) ja Torrents (torrentid) õigused", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Otsi ainult freeleech-väljalaskeid", + "IndexerGazelleGamesSettingsSearchGroupNames": "Otsi gruppide nimesid", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Otsi väljalaskeid gruppide nimede järgi", + "IndexerHDBitsSettingsCodecs": "Koodekid", + "IndexerHDBitsSettingsCodecsHelpText": "Kui pole määratud, kasutatakse kõiki valikuid.", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Kuva ainult freeleech-väljalaskeid", + "IndexerHDBitsSettingsMediums": "Andmekandjad", + "IndexerHDBitsSettingsMediumsHelpText": "Kui pole määratud, kasutatakse kõiki valikuid.", + "IndexerHDBitsSettingsOrigins": "Algallikad", + "IndexerHDBitsSettingsOriginsHelpText": "Kui pole määratud, kasutatakse kõiki valikuid.", + "IndexerHDBitsSettingsPasskeyHelpText": "Pääsuvõti kasutaja andmetest", + "IndexerHDBitsSettingsUseFilenames": "Kasuta failinimesid", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Märgi see kast, kui soovid kasutada torrenti failinimesid väljalaske pealkirjadena", + "IndexerHDBitsSettingsUsernameHelpText": "Lehe kasutajanimi", + "IndexerHealthCheckNoIndexers": "Ühtegi indekserit pole sisse lülitatud, {appName} ei tagasta otsingutulemusi", + "IndexerHistoryLoadError": "Viga indekseri ajaloo laadimisel", + "IndexerIPTorrentsSettingsCookieUserAgent": "Küpsiste kasutajaagent", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "Brauseri küpsisega seotud kasutajaagent", + "IndexerIPTorrentsSettingsSearchSortBy": "Otsingu järjestuse alus", + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Vali otsingutulemuste järjestamise viis", + "IndexerId": "Indekseri ID", + "IndexerInfo": "Indekseri andmed", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Kõik indekserid on tõrgete tõttu olnud kättesaamatud üle 6 tunni", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indekserid on tõrgete tõttu olnud kättesaamatud üle 6 tunni: {indexerNames}", + "IndexerMTeamTpSettingsApiKeyHelpText": "Veebilehe API-võti (Kasutaja seaded => Labor)", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Otsi ainult freeleech-väljalaskeid", + "IndexerName": "Indekseri nimi", + "IndexerNebulanceSettingsApiKeyHelpText": "API-võti: Kasutaja seaded > API võtmed. Vajalikud on õigused List ja Download", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Newznabi lisaparameetrid", + "IndexerNewznabSettingsApiKeyHelpText": "Veebilehe API-võti", + "IndexerNewznabSettingsVipExpirationHelpText": "Sisesta VIP-staatuse aegumise kuupäev (aaaa-kk-pp) või jäta väli tühjaks ({appName} teavitab nädal enne aegumist)", + "IndexerNoDefinitionCheckHealthCheckMessage": "Indekseritel puuduvad määratlused ja need ei tööta: {indexerNames}. Palun eemalda ja (või) lisa need uuesti rakendusse {appName}.", + "IndexerNzbIndexSettingsApiKeyHelpText": "Veebilehe API-võti (mittenõutav — otsida saab ilma võtmeta, kuid madalama limiidiga)", + "IndexerObsoleteCheckMessage": "Indekserid on vananenud või neid on uuendatud: {0}. Palun eemalda need ja/või lisa uuesti rakendusse {appName}", + "IndexerOrpheusSettingsApiKeyHelpText": "Veebilehe API-võti (leiad: Seaded => Juurdepääsuseaded)", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Veebilehe API-võti", + "IndexerPassThePopcornSettingsApiUserHelpText": "Need seaded asuvad sinu PassThePopcorn-turvaseadetes (Muuda profiili > Turvalisus).", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Otsi ainult freeleech-väljalaskeid", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Ainult Golden Popcorn", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Otsi ainult Golden Popcorn-väljalaskeid", + "IndexerPriority": "Indekseri prioriteet", + "IndexerPriorityHelpText": "Indekseri prioriteet vahemikus 1 (kõrgeim) kuni 50 (madalaim). Vaikimisi: 25.", + "IndexerProxies": "Indekseri proksid", + "IndexerProxy": "Indekseri proksi", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Ükski indekseri proksi pole tõrgete tõttu saadaval", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Indekseri proksid on tõrgete tõttu kättesaamatud: {indexerProxyNames}", + "IndexerQuery": "Indekseri päring", + "IndexerRedactedSettingsApiKeyHelpText": "Veebilehe API-võti (leiad: Seaded => Juurdepääsuseaded)", + "IndexerRss": "Indekseri RSS", + "IndexerSettingsAdditionalParameters": "Lisaparameetrid", + "IndexerSettingsApiPath": "API asukoht", + "IndexerSettingsApiPathHelpText": "API tee, tavaliselt {url}", + "IndexerSettingsApiUser": "API kasutaja", + "IndexerSettingsAppsMinimumSeeders": "Rakenduste minimaalne jagajate arv", + "IndexerSettingsAppsMinimumSeedersHelpText": "Rakenduste nõutav minimaalne jagajate arv, et indekser saaks faili haarata; tühja välja puhul kasutatakse sünkimisprofiili vaikeväärtust", + "IndexerSettingsBaseUrl": "Baas-URL", + "IndexerSettingsBaseUrlHelpText": "Vali baasaadress, mida {appName} kasutab veebilehele päringute saatmiseks", + "IndexerSettingsCookie": "Küpsis", + "IndexerSettingsCookieHelpText": "Veebilehe küpsis", + "IndexerSettingsFreeleechOnly": "Ainult Freeleech", + "IndexerSettingsGrabLimit": "Haaramise limiit", + "IndexerSettingsGrabLimitHelpText": "Maksimaalne haaramiste arv vastavalt valitud ühikule, mida {appName} saidil lubab", + "IndexerSettingsLimitsUnit": "Limiidi ühik", + "IndexerSettingsLimitsUnitHelpText": "Ajaühik indekseri limiidi arvestamiseks", + "IndexerSettingsPackSeedTime": "Paketi jagamisaeg", + "IndexerSettingsPackSeedTimeIndexerHelpText": "Aeg, mil paketti (hooaega või diskograafiat) tuleb enne peatamist jagada; tühja välja puhul kasutatakse rakenduse vaikeväärtust", + "IndexerSettingsPasskey": "Pääsuvõti", + "IndexerSettingsPreferMagnetUrl": "Eelista Magnet-linki", + "IndexerSettingsPreferMagnetUrlHelpText": "Kui lubatud, eelistab indekser haaramisel magnet-linke ning kasutab torrenti linke vaid varuvariandina", + "IndexerSettingsQueryLimit": "Päringute piirarv", + "IndexerSettingsQueryLimitHelpText": "Maksimaalne päringute piirarv vastavalt ühikule, mida {appName} lehele lubab", + "IndexerSettingsRssKey": "RSS-võti", + "IndexerSettingsSeedRatio": "Jagamise suhe", + "IndexerSettingsSeedRatioHelpText": "Jagamise suhe, mille torrent peab enne peatamist saavutama; tühja välja puhul kasutatakse allalaadimiskliendi vaikeväärtust. Suhe peaks olema vähemalt 1.0 ja järgima indekseri reegleid", + "IndexerSettingsSeedTime": "Jagamisaeg", + "IndexerSettingsSeedTimeHelpText": "Aeg, mil torrentit tuleb enne peatamist jagada; tühja välja puhul kasutatakse allalaadimiskliendi vaikeväärtust", + "IndexerSettingsSummary": "Seadista indekseri üldseadeid, sealhulgas proksisid.", + "IndexerSettingsVipExpiration": "VIP-staatuse aegumine", + "IndexerSite": "Indekseri veebileht", + "IndexerStatus": "Indekseri olek", + "IndexerStatusAllUnavailableHealthCheckMessage": "Ükski indekser pole tõrgete tõttu saadaval", + "IndexerStatusUnavailableHealthCheckMessage": "Tõrgete tõttu kättesaamatud indekserid: {indexerNames}", + "IndexerTagsHelpText": "Kasuta silte (tags), et määrata indekseri proksisid või rakendusi, millega indekser on sünkroonitud.", + "IndexerTagsHelpTextWarning": "Silte tuleks kasutada ettevaatlikult, sest neil võib olla ettenägematuid tagajärgi. Sildiga indekser sünkroonitakse ainult sama sildiga märgitud rakendustega.", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Veebilehe API-võti", + "IndexerVipExpiredHealthCheckMessage": "Indekseri VIP-hüved on aegunud: {indexerNames}", + "IndexerVipExpiringHealthCheckMessage": "Indekseri VIP-hüved on aegumas: {indexerNames}", + "Indexers": "Indekserid", + "MinimumSeedersHelpText": "Minimaalne jagajate arv, mis on vajalik rakendusele indekserist allalaadimise alustamiseks", + "NoIndexerCategories": "Selle indekseerija kategooriaid ei leitud", + "NoIndexerHistory": "Selle indekseerija ajalugu ei leitud", + "NoIndexersFound": "Ühtegi indekserit ei leitud", + "PreferMagnetUrlHelpText": "Kui lubatud, eelistab indekser haaramisel magnet-linke ning kasutab torrenti linke vaid varuvarandina", + "ProwlarrSupportsAnyIndexer": "{appName} toetab paljusid indeksereid lisaks kõigile neile, mis kasutavad Newznab/Torznab standardit läbi \"Standardne Newznab\" (useneti jaoks) või \"Standardne Torznab\" (torrentite jaoks) valiku. Otsi ja vali oma indekser altpoolt.", + "RedirectHelpText": "Suuna indekseri sissetulev allalaadimispäring ümber ja edasta haaramine otse, ilma seda läbi rakenduse {appName} vahendamata", + "RssIsNotSupportedWithThisIndexer": "See indekser ei toeta RSS-i", + "SearchAllIndexers": "Otsi kõigist indekseritest", + "SearchCountIndexers": "Otsi {count} indekserist", + "SearchIndexers": "Otsi indekseritest", + "SelectIndexers": "Vali indekserid", + "SettingsIndexerLogging": "Detailsem indekseri logimine", + "SettingsIndexerLoggingHelpText": "Logi täiendavaid indekseri andmeid koos vastusega", + "SyncAppIndexers": "Sünkrooni rakenduse indekserid", + "SyncLevelAddRemove": "Ainult lisamine ja eemaldamine: kui indeksereid lisatakse rakendusse {appName} või eemaldatakse sealt, värskendatakse seda kaugrakendust.", + "SyncLevelFull": "Täielik sünkroonimine: hoiab selle rakenduse indekserid täielikult sünkroonis. Rakenduses {appName} tehtud muudatused sünkroonitakse sellesse rakendusse. Kõik selle rakenduse indekserites tehtud muudatused kirjutatakse järgmise sünkroonimise ajal {appName} poolt üle.", + "TagsHelpText": "Kehtib indekseritele, millel on vähemalt üks kattuv silt", + "TestAllIndexers": "Testi kõiki indeksereid", + "TotalIndexerQueries": "Indekseri päringute üldarv", + "TotalIndexerSuccessfulGrabs": "Indekseri edukate allalaadimiste koguarv", + "UnableToAddANewIndexerPleaseTryAgain": "Uue indekseri lisamine nurjus, palun proovi uuesti.", + "UnableToAddANewIndexerProxyPleaseTryAgain": "Uue indekseri proksi lisamine nurjus, palun proovi uuesti.", + "UnableToLoadIndexerProxies": "Indekseri prokside laadimine nurjus", + "UnableToLoadIndexers": "Indekserite laadimine nurjus", + "AddCategory": "Lisa kategooria", + "AddConnection": "Lisa ühendus", + "AddConnectionImplementation": "Lisa ühendus - {implementationName}", + "AddCustomFilter": "Lisa kohandatud filter", + "AddDownloadClient": "Lisa allalaadimisklient", + "AddDownloadClientImplementation": "Lisa allalaadimisklient – {implementationName}", + "AddDownloadClientToProwlarr": "Allalaadimiskliendi lisamine võimaldab rakendusel {appName} saata väljalaskeid otse kasutajaliidesest käsitsi otsingu ajal.", + "AddRemoveOnly": "Ainult lisamine ja eemaldamine", + "AddSyncProfile": "Lisa sünkroonimisprofiil", + "AddToDownloadClient": "Lisa väljalase allalaadimisklienti", + "Added": "Lisatud", + "AddingTag": "Sildi lisamine", + "Age": "Vanus", + "GrabRelease": "Haara väljalase", + "Grabbed": "Haaratud", + "ManualGrab": "Käsitsi haaramine", + "OverrideGrabModalTitle": "Eira piiranguid ja haara - {title}", + "AddedToDownloadClient": "Väljalase lisatud kliendile", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Sünkroonimine lükkab haaramisel tagasi blokeerimisloendis olevad torrendi räsid", + "AverageGrabs": "Keskmine haaramiste arv", + "DownloadClientSettingsPriorityItemHelpText": "Prioriteet üksuste haaramisel", + "GrabReleases": "Haara väljalasked", + "GrabTitle": "Haara nimetus", + "Grabs": "Haaramised", + "IncludeManualGrabsHelpText": "Kaasa rakenduses {appName} tehtud käsitsi haaramised", + "OnGrab": "Väljalaske haaramisel", + "OnGrabHelpText": "Väljalaske haaramisel", + "AdvancedSettingsHiddenClickToShow": "Lisaseaded on peidetud, vajuta kuvamiseks", + "AdvancedSettingsShownClickToHide": "Lisaseaded on nähtaval, vajuta peitmiseks", + "Album": "Album", + "All": "Kõik", + "AllSearchResultsHiddenByFilter": "Kõik otsingutulemused on rakendatud filtri tõttu peidetud.", + "Analytics": "Analüütika", + "AnalyticsEnabledHelpText": "Saada anonüümset kasutus- ja veateavet {appName} serveritesse. See sisaldab teavet veebilehitseja, kasutatavate {appName} veebiliidese lehtede, veateavituste ning operatsioonisüsteemi ja platvormi versiooni kohta. Kasutame seda teavet funktsioonide ja veaparanduste seadmiseks prioriteediks.", + "Any": "Mistahes", + "ApiKey": "API-võti", + "ApiKeyValidationHealthCheckMessage": "Palun värskenda oma API-võti vähemalt {length} tähemärgi pikkuseks. Seda saab teha seadete või seadistusfaili kaudu", + "AppDataDirectory": "AppData kaust", + "AppDataLocationHealthCheckMessage": "Uuendamine ei ole võimalik, vältimaks AppData kausta kustutamist uuendamise käigus", + "AppProfileInUse": "Rakenduse profiil on kasutusel", + "AppProfileSelectHelpText": "Rakenduste profiile kasutatakse rakenduse sünroonimisel RSS-i, automaatse ja interaktiivse otsingu seadistuste haldamiseks", + "AppSettingsSummary": "Rakendused ja seaded {appName} ja PVR-programmide vahelise suhtluse seadistamiseks", + "AppUpdated": "{appName} on uuendatud", + "AppUpdatedVersion": "{appName} on uuendati versioonile {version}. Uusimate muudatuste rakendamiseks tuleb {appName} uuesti laadida", + "Application": "Rakendus", + "ApplicationAuthPassword": "Autentimisparool", + "ApplicationAuthPasswordHelpText": "HTTP Basic Auth parool, mida {appName} kasutab rakendusele ligipääsemiseks. Seadista see, kui rakendus on Basic-autentimisega kaitstud pöördproksi taga.", + "AuthBasic": "Lihtne (veebilehitseja hüpikaken)", + "ApplicationAuthUsernameHelpText": "HTTP Basic Auth kasutajanimi, mida {appName} kasutab rakendusele ligipääsemiseks. Seadista see, kui rakendus on Basic-autentimisega kaitstud pöördproksi taga.", + "ApplicationAuthUsername": "Autentimise kasutajanimi", + "ApplicationLongTermStatusCheckAllClientMessage": "Ükski rakendus pole olnud tõrgete tõttu saadaval rohkem kui 6 tundi", + "ApplicationLongTermStatusCheckSingleClientMessage": "Tõrgete tõttu enam kui 6 tundi kättesaamatud rakendused: {0}", + "ApplicationStatusCheckAllClientMessage": "Ükski rakendus pole tõrgete tõttu saadaval", + "ApplicationStatusCheckSingleClientMessage": "Tõrgete tõttu kättesaamatud rakendused: {0}", + "ApplicationURL": "Rakenduse URL", + "ApplicationUrlHelpText": "Selle rakenduse väline URL koos protokolliga http(s)://, pordiga ja URL-i baasiga", + "Applications": "Rakendused", + "ApplicationsLoadError": "Rakenduste loendi laadimine nurjus", + "Apply": "Rakenda", + "ApplyChanges": "Rakenda muudatused", + "ApplyTags": "Rakenda sildid", + "ApplyTagsHelpTextAdd": "Lisa: lisa sildid olemasolevate siltide loendisse", + "ApplyTagsHelpTextHowToApplyApplications": "Kuidas rakendada silte valitud rakendustele", + "ApplyTagsHelpTextRemove": "Eemalda: Eemalda sisestatud sildid", + "ApplyTagsHelpTextReplace": "Asenda: Asenda sildid sisestatud siltidega (kõigi siltide eemaldamiseks jäta väli tühjaks)", + "AptUpdater": "Kasuta värskenduse paigaldamiseks paketihaldurit apt", + "AuthForm": "Vormid (sisselogimisleht)", + "Authentication": "Autentimine", + "AuthenticationMethod": "Autentimismeetod", + "AuthenticationMethodHelpText": "Nõua rakendusele {appName} juurdepääsuks kasutajanime ja parooli", + "AuthenticationMethodHelpTextWarning": "Palun vali sobiv autentimismeetod", + "AuthenticationRequired": "Vajalik on autentimine", + "AuthenticationRequiredHelpText": "Muuda päringute tüüpe, mis nõuavad autentimist. Ära muuda seda seadistust ilma riske mõistmata.", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Kinnita uus parool", + "AuthenticationRequiredPasswordHelpTextWarning": "Sisesta uus parool", + "AuthenticationRequiredUsernameHelpTextWarning": "Sisesta uus kasutajanimi", + "AuthenticationRequiredWarning": "Autentimata kaugpääsu vältimiseks nõuab {appName} nüüd autentimise sisselülitamist. Soovi korral saad autentimise kohalike aadresside jaoks välja lülitada.", + "Automatic": "Automaatne", + "AutomaticSearch": "Automaatne otsing", + "Backup": "Varukoopia", + "BackupFolderHelpText": "Suhtelised teed asuvad rakenduse {appName} AppData kaustas", + "BackupIntervalHelpText": "Automaatse varundamise sagedus", + "BackupNow": "Varunda kohe", + "BackupRetentionHelpText": "Säilitusajast vanemad varukoopiad kustutatakse automaatselt", + "Backups": "Varukoopiad", + "BackupsLoadError": "Varukoopiate laadimine nurjus", + "BeforeUpdate": "Enne uuendamist", + "BindAddress": "Sidumisaadress", + "BindAddressHelpText": "Kehtiv IP-aadress, localhost või „*” kõigi liideste jaoks", + "BlackholeFolderHelpText": "Kaust, kuhu {appName} salvestab {extension}-faili", + "Branch": "Haru", + "BranchUpdate": "Haru rakenduse {appName} uuendamiseks", + "BranchUpdateMechanism": "Välise uuendusmehhanismi kasutatav haru", + "BuiltIn": "Sisseehitatud", + "BypassProxyForLocalAddresses": "Möödu proksist kohalike aadresside korral", + "Cancel": "Tühista", + "CancelPendingTask": "Kas soovid kindlasti selle ootel oleva ülesande tühistada?", + "Category": "Kategooria", + "CertificateValidation": "Sertifikaadi kinnitamine", + "Clear": "Tühjenda", + "ClientPriority": "Kliendi prioriteet", + "Clone": "Klooni", + "CloneProfile": "Klooni profiil", + "Close": "Sulge", + "Component": "Komponent", + "ConnectSettings": "Teavitusteenuse seaded", + "ConnectionLost": "Ühendus katkes", + "ConnectionLostReconnect": "{appName} proovib automaatselt uuesti ühenduda, võid ka klõpsata allolevat nuppu \"Laadi uuesti\".", + "ConnectionLostToBackend": "{appName} kaotas ühenduse serveriga ja tuleb funktsionaalsuse taastamiseks uuesti laadida.", + "Connections": "Ühendused", + "CountDownloadClientsSelected": "Valitud {count} allalaadimisklient(i)", + "CurrentlyInstalled": "Hetkel paigaldatud", + "Custom": "Kohandatud", + "CustomFilter": "Kohandatud filter", + "CustomFilters": "Kohandatud filtrid", + "Database": "Andmebaas", + "DatabaseMigration": "Andmebaasi üleviimine", + "Date": "Kuupäev", + "Dates": "Kuupäevad", + "Default": "Vaikimisi", + "None": "Eimidagi", + "UnableToAddANewDownloadClientPleaseTryAgain": "Uue allalaadimiskliendi lisamine nurjus, palun proovi uuesti.", + "DownloadClientQbittorrentSettingsSequentialOrder": "Järjestikune allalaadimine", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Laadi failid alla järjestikku (qBittorrent 4.1.0+)", + "DownloadClientRTorrentSettingsUrlPath": "URL-asukoht", + "Label": "Nimi", + "DownloadClientQbittorrentSettingsContentLayout": "Sisu struktuur", + "Language": "Keel", + "AppsMinimumSeeders": "Rakenduste minimaalne jagajate arv", + "Delete": "Kustuta", + "Discord": "Discord", + "DisabledForLocalAddresses": "Välja lülitatud kohalike aadresside jaoks", + "Docker": "Docker", + "DockerUpdater": "Uuenduse saamiseks värskenda Dockeri konteinerit", + "Download": "Laadi alla", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Lisab Deluge JSON-URL-ile eesliite, vaata {url}", + "DownloadClientFreeboxSettingsApiUrl": "API URL", + "DownloadClientFreeboxSettingsHostHelpText": "Freeboxi hostinimi või hosti IP-aadress, vaikimisi '{url}' (töötab ainult samas võrgus olles)", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Sellele kaustale peab olema XBMC-st ligipääs", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Kas kasutada qBittorrenti seadistatud sisu struktuuri, torrentis olevat algset struktuuri või luua alati alamkaust (qBittorrent 4.3.2+)", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Valikuline asukoht allalaadimiste paigutamiseks; jäta tühjaks, et kasutada rTorrenti vaikeasukohta", + "EditSelectedDownloadClients": "Muuda valitud allalaadimiskliente", + "EnableInteractiveSearch": "Luba interaktiivne otsing", + "EnableInteractiveSearchHelpText": "Kasutatakse interaktiivse otsingu korral", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Otsi ainult freeleech-väljalaskeid", + "Track": "lugu", + "UsenetBlackholeNzbFolder": "Nzb kaust", + "DownloadClientFreeboxSettingsAppToken": "Rakenduse token", + "PackSeedTimeHelpText": "Aeg, mil paketti (hooaega või diskograafiat) tuleb enne peatamist jagada; tühja välja puhul kasutatakse rakenduse vaikeväärtust", + "Reload": "Laadi uuesti", + "SearchTypes": "Otsitavad tüübid", + "Season": "Hooaeg", + "SeedTime": "Jagamisaeg", + "SettingsEnableColorImpairedMode": "Luba värvipimedasõbralik režiim", + "SettingsEnableColorImpairedModeHelpText": "Kohandatud stiil paremaks värvieristuseks värvipimedusega kasutajatele", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Rakenduse token, mis saadi Freebox API-le juurdepääsu loomisel (ehk „app_token“)", + "UseSsl": "Kasuta turvalist ühendust (SSL)", + "EnableSSL": "Luba SSL", + "Enabled": "Lubatud", + "Encoding": "Kodeering", + "DefaultNameCopiedProfile": "{name} - koopia", + "DeleteBackup": "Kustuta varukoopia", + "DeleteDownloadClient": "Kustuta allalaadimisklient", + "DeleteTag": "Kustuta silt", + "Destination": "Sihtkoht", + "Directory": "Kataloog", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Valikuline jagatud kaust allalaadimiste jaoks (Download Stationi vaikimisi asukoha kasutamiseks jäta väli tühjaks)", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Lisab meedia omadused siltidena. Vihjed on näited.", + "DownloadClientFloodSettingsTagsHelpText": "Allalaadimise algsed sildid. Tuvastamiseks peavad allalaadimisel olema kõik algsed sildid. See aitab vältida konflikte seosetute allalaadimistega.", + "DownloadClientFloodSettingsAdditionalTags": "Lisasildid", + "DownloadClientFloodSettingsUrlBaseHelpText": "Lisab Flood API-le eesliide, näiteks {url}", + "DownloadClientFreeboxSettingsPortHelpText": "Port, mida kasutatakse Freeboxi liidesele juurdepääsuks, vaikimisi '{port}'", + "DownloadClientPneumaticSettingsNzbFolder": "Nzb kaust", + "DownloadClientPneumaticSettingsStrmFolder": "Strm kaust", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Selle kausta .strm-failid impordib droon", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "qBittorrenti lisatud torrentite algolek. Arvesta, et jõuga käivitatud torrentid ei allu jagamise piirangutele.", + "DownloadClientRTorrentSettingsAddStopped": "Lisa peatunult", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Selle lubamine lisab torrentid ja magnetlingid rTorrentisse seisatud olekus. See võib magnetfailid rikkuda.", + "Enable": "Luba", + "Queue": "Järjekord", + "SeedRatio": "Jagamise suhe", + "UnableToAddANewNotificationPleaseTryAgain": "Uue teavituse lisamine nurjus, palun proovi uuesti.", + "EditConnectionImplementation": "Muuda ühendust - {implementationName}", + "EditDownloadClientImplementation": "Muuda allalaadimisklienti - {implementationName}", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Nõuab NzbGeti versiooni 16.0 või uuemat", + "Status": "Olek", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Kasuta turvalist ühendust. Vaata qBittorrenti sätteid: Valikud -> WebUI -> „Kasuta HTTP asemel HTTPS-i“.", + "Details": "Üksikasjad", + "NetCore": ".NET", + "Scheduled": "Ajastatud", + "Title": "Nimetus", + "Artist": "esitaja", + "Info": "Info", + "PackSeedTime": "Paketi jagamisaeg", + "EnableAutomaticSearch": "Luba automaatne otsing", + "DeleteSelectedDownloadClients": "Kustuta allalaadimiskliendid", + "Host": "Host", + "Source": "Allikas", + "UILanguageHelpTextWarning": "Vajalik on veebilehitseja uuestilaadimine", + "EnableAutomaticSearchHelpText": "Kasutatakse automaatsete otsingute korral, mida käivitatakse kasutajaliidesest või {appName} poolt", + "DownloadClientAriaSettingsDirectoryHelpText": "Valikuline allalaadimiskaust. Jäta tühjaks, et kasutada Aria2 vaikimisi asukohta", + "Port": "Port", + "Protocol": "Protokoll", + "Donate": "Anneta", + "Donations": "Annetused", + "DownloadClient": "Allalaadimisklient", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Laadi esmalt alla esimene ja viimane tükk (qBittorrent 4.1.0+)", + "VipExpiration": "VIP-staatuse aegumine", + "EnableSslHelpText": " Jõustamiseks tuleb rakendus administraatorina taaskäivitada", + "Ended": "Lõppenud", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Esimene ja viimane kõigepealt", + "DeleteBackupMessageText": "Kas oled kindel, et soovid kustutada varukoopia '{name}'?", + "Episode": "Episood", + "EnableRss": "Luba RSS", + "Filter": "Filter", + "NotificationsEmailSettingsUseEncryptionHelpText": "Kas eelistada krüptimist, kui see on serveris seadistatud, kasutada alati krüptimist SSL-i (ainult port 465) või StartTLS-i (mis tahes muu port) kaudu, või mitte kunagi krüptimist kasutada", + "Filters": "Filtrid", + "DeleteDownloadClientMessageText": "Kas oled kindel, et soovid kustutada allalaadimiskliendi '{name}'?", + "DeleteNotification": "Kustuta teavitus", + "PreferMagnetUrl": "Eelista Magnet-linki", + "DeleteNotificationMessageText": "Kas oled kindel, et soovid kustutada teavituse '{name}'?", + "DeleteSelectedDownloadClientsMessageText": "Kas oled kindel, et soovid kustutada {count} valitud allalaadimisklienti?", + "DeleteTagMessageText": "Kas oled kindel, et soovid kustutada sildi '{label}'?", + "Disabled": "Välja lülitatud", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Määrab Freebox API baasi URL-i koos API versiooniga, nt „{url}“, vaikeväärtus on „{defaultApiUrl}“", + "DownloadClientFreeboxSettingsAppId": "Rakenduse ID", + "DownloadClientFreeboxSettingsAppIdHelpText": "Freebox API juurde juurdepääsu loomisel antud rakenduse ID (nt „app_id“)", + "DownloadClientSettings": "Allalaadimiskliendi seaded", + "DownloadClientSettingsAddPaused": "Lisa peatatult", + "DownloadClientSettingsDestinationHelpText": "Määrab sihtkoha käsitsi, vaikeväärtuse kasutamiseks jäta tühjaks", + "DownloadClientSettingsInitialState": "Algolek", + "DownloadClientSettingsInitialStateHelpText": "Klienti {clientName} lisatud torrentite algolek", + "DownloadClients": "Allalaadimiskliendid", + "DownloadClientsLoadError": "Allalaadimisklientide laadimine nurjus", + "Duration": "Kestus", + "Edit": "Muuda", + "Error": "Viga", + "ErrorLoadingContents": "Viga sisu laadimisel", + "ErrorRestoringBackup": "Tõrge varukoopia taastamisel", + "EventType": "Sündmuse liik", + "Events": "Sündmused", + "Exception": "Erand", + "ExistingTag": "Olemasolev silt", + "External": "Väline", + "ExternalUpdater": "{appName} on seadistatud kasutama välist uuendusmehhanismi", + "Failed": "Nurjus", + "FailedToFetchSettings": "Seadete hankimine nurjus", + "FailedToFetchUpdates": "Uuenduste hankimine nurjus", + "FeatureRequests": "Uute funktsioonide taotlused", + "Filename": "Failinimi", + "Files": "Failid", + "Fixed": "Parandatud", + "Folder": "Kaust", + "General": "Üldine", + "GeneralSettings": "Üldseaded", + "Health": "Seisund", + "DownloadClientUTorrentProviderMessage": "uTorrent on minevikus kaasanud krüptokaevandajaid, pahavara ja reklaame, seetõttu soovitame valida mõne muu rakenduse.", + "DownloadClientSettingsUrlBaseHelpText": "Lisab rakenduse {clientName} URL-ile eesliite (nt {url})", + "DownloadClientSettingsUseSslHelpText": "Kasuta turvalist ühendust rakendusega {clientName} ühenduse loomisel", + "DownloadClientStatusAllClientHealthCheckMessage": "Kõik allalaadimiskliendid on tõrgete tõttu kättesaamatud", + "DownloadClientStatusSingleClientHealthCheckMessage": "Tõrgete tõttu kättesaamatud allalaadimiskliendid: {downloadClientNames}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Valikuline allalaadimiskoht, Transmissioni vaikeasukoha kasutamiseks jäta tühjaks", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisab rakenduse {clientName} RPC URL-ile eesliite (nt {url}), vaikeväärtus on '{defaultUrl}'", + "HealthMessagesInfoBox": "Lisateavet nende seisundikontrolli teadete põhjuse kohta leiad, klõpsates rea lõpus asuvat viki linki (raamatu ikooni) või kontrollides oma [logisid]({link}). Kui vajad teadete mõistmisel abi, võta ühendust meie kasutajatoega allolevatel linkidel.", + "NotificationStatusAllClientHealthCheckMessage": "Kõik teavitused on tõrgete tõttu kättesaamatud", + "NotificationStatusSingleClientHealthCheckMessage": "Tõrgete tõttu kättesaamatud teavitused: {notificationNames}", + "OnHealthIssue": "Seisundiprobleemi korral", + "OnHealthRestored": "Probleemi lahenemisel", + "ProxyFailedToTestHealthCheckMessage": "Proksi testimine nurjus: {url}", + "ProxyResolveIpHealthCheckMessage": "Seadistatud proksiserveri nimele ({proxyHostName}) vastavat IP-aadressi ei õnnestunud tuvastada", + "SystemTimeHealthCheckMessage": "Süsteemiaeg erineb enam kui 1 päeva võrra. Ajastatud toimingud ei pruugi toimida korrektselt enne aja parandamist", + "UpdateAvailableHealthCheckMessage": "Saadaval on uuendus: {version}", + "UpdateStartupNotWritableHealthCheckMessage": "Uuendust ei saa paigaldada: kasutajal '{userName}' puudub kirjutamisõigus käivituskaustas {startupFolder}{userName}.", + "HideAdvanced": "Peida lisaseaded", + "History": "Ajalugu", + "HomePage": "Avaleht", + "Hostname": "Hostinimi", + "IgnoredAddresses": "Eiratud aadressid", + "Implementation": "Teostus", + "InfoUrl": "Info URL", + "Install": "Paigalda", + "InstallLatest": "Paigalda uusim", + "InstallMajorVersionUpdate": "Paigalda uuendus", + "InstallMajorVersionUpdateMessage": "See uuendus paigaldab uue põhiversiooni ega pruugi sinu süsteemiga ühilduda. Kas oled kindel, et soovid jätkata?", + "InstallMajorVersionUpdateMessageLink": "Lisainfot leiad [{domain}]({url}).", + "InstanceName": "Instantsi nimi", + "InstanceNameHelpText": "Instantsi nimi vahekaardil ja Syslogi rakenduse nimena", + "InteractiveSearch": "Interaktiivne otsing", + "Interval": "Intervall", + "InvalidUILanguage": "Kasutajaliidese keeleks on määratud vigane keel, parandai see ja salvesta seaded", + "KeyboardShortcuts": "Kiirklahvid", + "LabelIsRequired": "Nimi on vajalik", + "LastDuration": "Eelmine kestus", + "LastExecution": "Eelmine täitmine", + "LastWriteTime": "Viimati salvestatud", + "Level": "Tase", + "LogFiles": "Logifailid", + "LogFilesLocation": "Logifailide asukoht: {location}", + "LogLevel": "Logimise tase", + "LogLevelTraceHelpTextWarning": "Jälituslogimine (trace) tuleks sisse lülitada vaid ajutiselt", + "LogSizeLimit": "Logi suuruse piirang", + "LogSizeLimitHelpText": "Logifaili maksimaalne suurus (MB) enne arhiveerimist. Vaikimisi 1 MB.", + "Logging": "Logimine", + "Logout": "Logi välja", + "Logs": "Logid", + "MaintenanceRelease": "Hooldusväljalase: veaparandused ja muud täiustused. Lisateavet leiab GitHubi koodimuudatuste ajaloost", + "ManageClients": "Halda kliente", + "ManageDownloadClients": "Halda allalaadimiskliente", + "Manual": "Käsitsi", + "Mechanism": "Mehhanism", + "Menu": "Menüü", + "Message": "Sõnum", + "Mixed": "Segatud", + "Mode": "Režiim", + "More": "Veel", + "MoreInfo": "Lisateave", + "Name": "Nimi", + "New": "Uus", + "NextExecution": "Järgmine täitmine", + "No": "Ei", + "NoBackupsAreAvailable": "Varukoopiaid pole saadaval", + "NoChange": "Muutusi pole", + "NoChanges": "Muudatusi pole", + "NoDownloadClientsFound": "Allalaadimiskliente ei leitud", + "NoEventsFound": "Sündmusi ei leitud", + "NoHistoryFound": "Ajalugu ei leitud", + "NoIssuesWithYourConfiguration": "Sinu seadistusega pole probleeme", + "NoLeaveIt": "Ei, las jääb", + "NoLogFiles": "Logifaile pole", + "NoTagsHaveBeenAddedYet": "Ühtegi silti pole veel lisatud", + "NoUpdatesAreAvailable": "Uuendusi pole saadaval", + "NotificationTriggers": "Teavituste käivitajad", + "NotificationTriggersHelpText": "Vali sündmused, mis selle teavituse saatma peaksid", + "Ok": "Ok", + "OnApplicationUpdate": "Rakenduse uuendamisel", + "OnLatestVersion": "Rakenduse {appName} uusim versioon on juba paigaldatud", + "Options": "Valikud", + "NotificationsEmailSettingsUseEncryption": "Kasuta krüptimist", + "NotificationsTelegramSettingsIncludeAppName": "Lisa pealkirja {appName}", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Soovi korral lisa teavituse pealkirja ette {appName}, et eristada eri rakenduste teavitusi", + "OpenBrowserOnStart": "Ava käivitamisel veebilehitseja", + "PackageVersion": "Paketi versioon", + "PackageVersionInfo": "{packageVersion} looja {packageAuthor}", + "Password": "Parool", + "PasswordConfirmation": "Parooli kinnitus", + "Peers": "Peerid", + "PendingChangesDiscardChanges": "Tühista muudatused ja lahku", + "PendingChangesMessage": "Lehel on salvestamata muudatusi, kas soovid kindlasti lahkuda?", + "PendingChangesStayReview": "Jää ja vaata muudatused üle", + "PortNumber": "Pordi number", + "Presets": "Eelseadistused", + "PreviouslyInstalled": "Varem paigaldatud", + "Priority": "Prioriteet", + "PrioritySettings": "Prioriteet: {priority}", + "Remove": "Eemalda", + "RemoveFilter": "Eemalda filter", + "RemovedFromTaskQueue": "Eemaldati ülesannete järjekorrast", + "RemovingTag": "Sildi eemaldamine", + "GeneralSettingsSummary": "Port, SSL, kasutajanimi/parool, proksiserver, analüütika ja uuendused", + "ProxyValidationBadRequest": "Proksi testimine nurjus. Olekukood: {statusCode}", + "OnHealthRestoredHelpText": "Probleemi lahenemisel", + "UnableToLoadGeneralSettings": "Üldseadete laadimine nurjus", + "UnableToLoadHistory": "Ajaloo laadimine nurjus", + "MinimumSeeders": "Minimaalne jagajate arv", + "NoLinks": "Linke pole", + "ProxyBadRequestHealthCheckMessage": "Proksi testimine nurjus. Olekukood: {statusCode}", + "CloseCurrentModal": "Sulge praegune aken", + "StartTypingOrSelectAPathBelow": "Alusta tippimist või vali allpool", + "OnHealthIssueHelpText": "Seisundiprobleemi korral", + "SaveSettings": "Salvesta seaded", + "IncludeHealthWarningsHelpText": "Kaasa seisundihoiatused", + "LaunchBrowserHelpText": " Ava rakenduse käivitamisel veebilehitsejas {appName} koduleht.", + "days": "päeva", + "minutes": "minutit", + "OpenThisModal": "Ava see aken", + "FocusSearchBox": "Aktiveeri otsinguväli", + "AllowedHosts": "Lubatud hostid", + "AllowedHostsHelpText": "Komaga eraldatud loend hostinimedest ja IP-aadressidest, mille päringuid {appName} aktsepteerib. Alamdomeenide asendussümbolina kasuta kuju '*.'. Kõikide hostide lubamiseks jäta tühjaks.", + "AllowedHostsNotConfiguredHealthCheckMessage": "Lubatud hostid pole seadistatud, {appName} võtab vastu päringuid mis tahes hostinimelt. Määra üldseadetes „Lubatud hostid“, et piirata, milliste hostinimede kaudu saab rakendusele {appName} ligi.", + "Author": "Autor", + "Categories": "Kategooriad", + "OnApplicationUpdateHelpText": "Rakenduse uuendamisel", + "SettingsLongDateFormat": "Pikk kuupäevavorming", + "TrustedNetworksHelpText": "Komadega eraldatud IP-aadresside või võrkude loend CIDR-notatsioonis, kus usaldusväärsed pöördproksid asuvad. Nt 172.17.0.1, 10.0.0.0/8 või fc00::/7. Lisa ainult proksid, mis on õigesti konfigureeritud edastama korrektseid päiseid..", + "Proxy": "Proksi", + "ProxyBypassFilterHelpText": "Kasuta eraldajana koma ',' ja alamdomeenide puhul tärni '*.'", + "ProxyPasswordHelpText": "Kasuta kasutajanime ja parooli ainult siis, kui neid nõutakse. Muul juhul jäta need tühjaks.", + "ProxyType": "Proksiserveri tüüp", + "ProxyUsernameHelpText": "Kasuta kasutajanime ja parooli ainult siis, kui neid nõutakse. Muul juhul jäta need tühjaks.", + "PublishedDate": "Avaldamise kuupäev", + "Queued": "Järjekorras", + "ReadTheWikiForMoreInformation": "Lisateabe saamiseks loe Vikit", + "RecentChanges": "Uusimad muudatused", + "Refresh": "Värskenda", + "Replace": "Asenda", + "Reset": "Lähtesta", + "ResetAPIKey": "Lähtesta API-võti", + "ResetAPIKeyMessageText": "Kas oled kindel, et soovid oma API-võtme lähtestada?", + "Restart": "Taaskäivita", + "RestartNow": "Taaskäivita kohe", + "RestartReloadNote": "Märkus: {appName} käivitub uuesti ja laadib kasutajaliidese taastamisprotsessi käigus automaatselt uuesti.", + "RestartRequiredHelpTextWarning": "Jõustumiseks on vaja taaskäivitada", + "Restore": "Taasta", + "RestoreBackup": "Taasta varukoopia", + "Result": "Tulemus", + "Retention": "Säilitamine", + "Rss": "RSS", + "Save": "Salvesta", + "SaveChanges": "Salvesta muudatused", + "Script": "Skript", + "ScriptPath": "Skripti asukoht", + "Search": "Otsi", + "SecretToken": "Salatoken", + "DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC-teenuse asukoht (vt {url}). ruTorrenti puhul on selleks enamasti RPC2 või [ruTorrent'i tee]{url2}.", + "Security": "Turvalisus", + "Seeders": "Jagajad", + "SelectAll": "Vali kõik", + "SelectDownloadClientModalTitle": "{modalTitle} – Vali allalaadimisklient", + "SendAnonymousUsageData": "Saada anonüümseid kasutusandmeid", + "SetTags": "Määra sildid", + "Settings": "Seaded", + "ShowAdvanced": "Kuva lisaseaded", + "ShowSearch": "Kuva otsing", + "ShowSearchHelpText": "Kuva otsingunupp hiirega peale liikumisel", + "Shutdown": "Seiska", + "Size": "Suurus", + "TestAllClients": "Testi kõiki kliente", + "Theme": "Teema", + "Time": "Aeg", + "Today": "Täna", + "Tomorrow": "Homme", + "TorrentBlackholeSaveMagnetFiles": "Salvesta magnetfailid", + "TorrentBlackholeSaveMagnetFilesExtension": "Salvesta magnetfailide laiendus", + "TorrentBlackholeTorrentFolder": "Torrenti kaust", + "Torrents": "Torrentid", + "TrustedNetworks": "Usaldusväärsed võrgud", + "Type": "Tüüp", + "Started": "Käivitati", + "StartupDirectory": "Käivituskaust", + "StopSelecting": "Lõpeta valimine", + "Style": "Stiil", + "System": "Süsteem", + "TableOptions": "Tabeli valikud", + "TagIsNotUsedAndCanBeDeleted": "Silti ei kasutata ja selle saab kustutada", + "Tags": "Sildid", + "TagsSettingsSummary": "Vaata silte ja nende kasutust. Kasutamata sildid saab eemaldada", + "Tasks": "Ülesanded", + "Test": "Testi", + "TestAll": "Testi kõiki", + "TheLogLevelDefault": "Logitase on vaikimisi 'Silumine' ja seda saab muuta valikus [Üldseaded](/settings/general)", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnetlinkide jaoks kasutatav laiend (vaikimisi '.magnet')", + "TorrentBlackholeSaveMagnetFilesHelpText": "Salvesta magnet-link, kui .torrent-fail pole saadaval (kasulik vaid juhul, kui allalaadimisklient toetab magnet-linkide salvestamist faili)", + "Sort": "Järjestus", + "UnsavedChanges": "Salvestamata muudatused", + "UnselectAll": "Tühista kõigi valik", + "UpdateAppDirectlyLoadError": "{appName} otse uuendamine nurjus,", + "UpdateAutomaticallyHelpText": "Laadi ja paigalda uuendused automaatselt. Saad neid endiselt paigaldada ka valikust Süsteem: Uuendused", + "UpdateMechanismHelpText": "Kasuta rakenduse {appName} sisseehitatud uuendajat või skripti", + "UpdateScriptPathHelpText": "Kohandatud skripti failitee, mis töötleb lahtipakitud uuenduse ja viib protsessi lõpule", + "UpdateStartupTranslocationHealthCheckMessage": "Värskendust ei saa paigaldada, kuna käivituskaust '{startupFolder}' asub App Translocation-kaustas.", + "UpdaterLogFiles": "Uuendaja logifailid", + "Updates": "Uuendused", + "Uptime": "Tööaeg", + "UrlBaseHelpText": "Pöördproksi jaoks, vaikimisi tühi", + "UseProxy": "Kasuta proksiserverit", + "Usenet": "Usenet", + "Username": "Kasutajanimi", + "Version": "Versioon", + "Warn": "Hoiata", + "WhatsNew": "Mis on uut?", + "Wiki": "Wiki", + "WouldYouLikeToRestoreBackup": "Kas soovid taastada varukoopia '{name}'?", + "XmlRpcPath": "XML RPC failitee", + "Year": "Aasta", + "Yes": "Jah", + "YesCancel": "Jah, tühista", + "Yesterday": "Eile", + "View": "Vaata", + "ChangeHasNotBeenSavedYet": "Muudatust pole veel salvestatud", + "CouldNotConnectSignalR": "SignalR-iga ühenduse loomine nurjus, kasutajaliidest ei värskendata", + "Apps": "Rakendused", + "AreYouSureYouWantToDeleteCategory": "Kas oled kindel, et soovid seotud kategooria kustutada?", + "AudioSearch": "Otsi heli järgi", + "Auth": "Autentimine", + "AuthQueries": "Autentimispäringud", + "AverageQueries": "Päringuid keskmiselt", + "BasicSearch": "Tavaotsing", + "Book": "Raamat", + "BookSearch": "Raamatute otsing", + "CertificateValidationHelpText": "Määra HTTPS-sertifikaatide kontrollimise rangus", + "UISettingsSummary": "Kuupäeva, keele ja värvinägemise valikud", + "ClearHistory": "Tühjenda ajalugu", + "ClearHistoryMessageText": "Kas oled kindel, et soovid kogu rakenduse {appName} ajaloo kustutada?", + "ClickToChangeQueryOptions": "Klõpsa päringuvalikute muutmiseks", + "Columns": "Tulbad", + "Connect": "Teavitused", + "ConnectSettingsSummary": "Teavitused ja kohandatud skriptid", + "DeleteClientCategory": "Kustuta allalaadimiskliendi kategooria", + "UnableToLoadUISettings": "Kasutajaliidese seadete laadimine nurjus", + "Publisher": "Kirjastaja", + "SettingsShowRelativeDates": "Kuva kuupäevi suhtelisena", + "IllRestartLater": "Taaskäivitan hiljem", + "UISettings": "Kasutajaliidese seaded", + "ForMoreInformationOnTheIndividualDownloadClients": "Täpsema info saamiseks erinevate allalaadimisklientide kohta, vajuta lisainfo nuppudele.", + "Notifications": "Teavitused", + "UpdateUiNotWritableHealthCheckMessage": "Uuendust ei saa paigaldada, kuna kasutajal '{userName}' puudub kirjutamisõigus kasutajaliidese kausta '{uiFolder}'.", + "PageSizeHelpText": "Igal lehel kuvatavate üksuste arv", + "RestartProwlarr": "Taaskäivita {appName}", + "SettingsShowRelativeDatesHelpText": "Kuva suhtelised (täna/eile/jne) või absoluutsed kuupäevad", + "UILanguageHelpText": "Keel, mida {appName} kasutajaliidese jaoks kasutab", + "UnableToLoadTags": "Siltide laadimine nurjus", + "SSLPort": "SSL-port", + "UI": "Kasutajaliides", + "SSLCertPassword": "SSL-sertifikaadi parool", + "SSLCertPasswordHelpText": "PFX-faili parool", + "SSLCertPath": "SSL-sertifikaadi asukoht", + "Torrent": "Torrent", + "UILanguage": "Kasutajaliidese keel", + "URLBase": "Baas-URL", + "SettingsShortDateFormat": "Lühike kuupäevavorming", + "PageSize": "Lehe suurus", + "UserAgentProvidedByTheAppThatCalledTheAPI": "API päringu teinud rakenduse esitatud User-Agent", + "SettingsTimeFormat": "Ajavorming", + "TableOptionsColumnsMessage": "Vali kuvatavad tulbad ja nende järjekord", + "HistoryCleanupDaysHelpText": "Automaatse tühjendamise keelamiseks määra väärtuseks 0", + "BookSearchTypes": "Raamatute otsingu tüübid", + "CountApplicationsSelected": "{count} rakendust valitud", + "DefaultCategory": "Vaikekategooria", + "DeleteAppProfile": "Kustuta rakenduse profiil", + "DeleteAppProfileMessageText": "Kas soovid kindlasti kustutada rakenduse profiili '{name}'?", + "DeleteApplication": "Kustuta rakendus", + "DeleteApplicationMessageText": "Kas soovid kindlasti kustutada rakenduse '{name}'?", + "DeleteSelectedApplications": "Kustuta valitud rakendused", + "DeleteSelectedApplicationsMessageText": "Kas soovid kindlasti kustutada {count} valitud rakendust?", + "Description": "Kirjeldus", + "DevelopmentSettings": "Arendusseaded", + "DisabledUntil": "Keelatud kuni", + "DownloadClientCategory": "Allalaadimisikliendi kategooria", + "DownloadClientPriorityHelpText": "Määra mitme allalaadimisikliendi prioriteedid. Sama prioriteediga klientide puhul kasutatakse koormuse jagamiseks vahelduvat eesõigusust (Round-Robin).", + "DownloadClientSettingsDefaultCategoryHelpText": "Vaikimisi varukategooria, kui väljaandele pole kategooriat määratud. Rakendusepõhise {appName} kategooria lisamine aitab vältida konflikte teiste allalaadimistega. Kategooria määramine on valikuline, kuid väga soovitatav.", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Vaikimisi varukategooria juhtudeks, kui väljaandel pole vastendatud kategooriat. Konkreetselt rakendusele {appName} mõeldud kategooria lisamine väldib konflikte muude allalaadimistega. Kategooria kasutamine on valikuline, kuid tugevalt soovitatav. Loob väljundkausta alamkausta [category].", + "DownloadClientsSettingsSummary": "Allalaadimisklientide seadistus rakenduse {appName} kasutajaliidese otsingusse integreerimiseks", + "EditApplicationImplementation": "Muuda rakendust - {implementationName}", + "EditCategory": "Muuda kategooriat", + "MIA": "Kadunud", + "OAuthPopupMessage": "Sinu veebibrauser blokeerib hüpikaknaid", + "Reddit": "Reddit", + "RefreshMovie": "Värskenda filmi", + "ReleaseBranchCheckOfficialBranchMessage": "Haru {0} ei ole kehtiv {appName}i väljalaskeharu, uuendusi ei saa", + "ReleaseStatus": "Ilmumise olek", + "SuggestTranslationChange": "Soovita tõlke muudatust", + "TagCannotBeDeletedWhileInUse": "Ei saa kustutada, kuni see on kasutusel" +} diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index a2dd5b818e8..79dbc12614b 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -118,7 +118,7 @@ "System": "Järjestelmä", "SystemTimeHealthCheckMessage": "Järjestelmän ajassa on ainakin vuorokauden heitto eivätkä ajoitetut tehtävät tämän vuoksi toimi oikein ennen kuin se on korjattu.", "TagCannotBeDeletedWhileInUse": "Tunnistetta ei voida poistaa kun se on käytössä.", - "TagIsNotUsedAndCanBeDeleted": "Tunniste ei ole käytössä ja voidaan poistaa.", + "TagIsNotUsedAndCanBeDeleted": "Tunniste on käyttämätön ja voidaan poistaa.", "TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa käyttämättömät tunnisteet.", "Tasks": "Tehtävät", "Test": "Koesta", @@ -778,7 +778,7 @@ "IndexerFileListSettingsUsernameHelpText": "Sivuston käyttäjätunnus", "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Etsi julkaisuja ryhmien nimillä.", "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Näytä vain Freeleech-julkaisut.", - "IndexerNzbIndexSettingsApiKeyHelpText": "Sivuston rajapinnan avain (valinnainen, haku toimii myös ilman, mutta sitä rajoitetaan).", + "IndexerNzbIndexSettingsApiKeyHelpText": "Sivuston rajapinnan avain (haku toimii myös ilman, mutta hakutiheyttä rajoitetaan).", "IndexerHDBitsSettingsOrigins": "Alkuperä", "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Etsi vain freeleech-julkaisuja.", "IndexerHDBitsSettingsUseFilenamesHelpText": "Käsittele torrentien tiedostonimiä julkaisujen nimikkeinä.", @@ -811,5 +811,7 @@ "ApplicationAuthPassword": "Salasana", "ApplicationAuthPasswordHelpText": "HTTP Basic Auth -salasana, jota {appName} käyttää kirjautuessaan sovellukseen. Aseta, jos sovellus on Basic Auth -suojausta käyttävän käänteisvälityspalvelimen takana.", "ApplicationAuthUsernameHelpText": "HTTP Basic Auth -tunnus, jota {appName} käyttää kirjautuessaan sovellukseen. Aseta, jos sovellus on Basic Auth -suojausta käyttävän käänteisvälityspalvelimen takana.", - "ApplicationAuthUsername": "Käyttäjätunnus" + "ApplicationAuthUsername": "Käyttäjätunnus", + "IndexerIPTorrentsSettingsSearchSortBy": "Tulosten järjestystapa", + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Valitse miten hakutulokset järjestetään." } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 06a4d344bdf..4c6ebdfafe7 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -22,8 +22,8 @@ "Clear": "Effacer", "BackupNow": "Sauvegarder maintenant", "Backup": "Sauvegarde", - "AppDataLocationHealthCheckMessage": "La mise à jour ne sera pas possible afin d'éviter la suppression des données d'application (AppData)", - "Analytics": "Statistiques", + "AppDataLocationHealthCheckMessage": "La mise à jour ne sera pas possible afin empêcher la suppression de AppData lors de la mise à jour", + "Analytics": "Analytiques", "All": "Tout", "About": "À propos", "IndexerStatusUnavailableHealthCheckMessage": "Indexeurs indisponibles en raison d'échecs : {indexerNames}", @@ -150,7 +150,7 @@ "IgnoredAddresses": "Adresses ignorées", "Hostname": "Nom d'hôte", "GeneralSettings": "Réglages généraux", - "Fixed": "Corrigés", + "Fixed": "Corrigé", "EnableSslHelpText": " Nécessite un redémarrage en tant qu'administrateur pour être effectif", "EnableSSL": "Activer le SSL", "EnableInteractiveSearch": "Activer la recherche interactive", @@ -683,7 +683,7 @@ "IndexerHDBitsSettingsCodecs": "Codecs", "IndexerHDBitsSettingsCodecsHelpText": "Si elle n'est pas spécifiée, toutes les options sont utilisées.", "IndexerHDBitsSettingsUseFilenamesHelpText": "Cochez cette option si vous souhaitez utiliser les noms de fichiers des torrents comme titres de publication", - "IndexerNzbIndexSettingsApiKeyHelpText": "Clé API du site", + "IndexerNzbIndexSettingsApiKeyHelpText": "Clé API du site (facultatif — la recherche fonctionne sans clé, mais avec un quota de requêtes plus limité)", "IndexerOrpheusSettingsApiKeyHelpText": "Clé API du site (dans Paramètres => Paramètres d'accès)", "IndexerPassThePopcornSettingsApiKeyHelpText": "Clé API du site", "IndexerPassThePopcornSettingsApiUserHelpText": "Ces paramètres se trouvent dans les paramètres de sécurité de PassThePopcorn (Modifier le profil > Sécurité).", @@ -746,7 +746,7 @@ "ClickToChangeQueryOptions": "Cliquez pour modifier les options de la requête", "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Si un torrent est bloqué par le hachage, il peut ne pas être correctement rejeté pendant le RSS/recherche pour certains indexeurs. L'activation de cette fonction permet de le rejeter après que le torrent a été saisi, mais avant qu'il ne soit envoyé au client.", "ProwlarrDownloadClientsAlert": "Si vous avez l'intention d'effectuer des recherches directement dans {appName}, vous devez ajouter les clients de téléchargement. Sinon, vous n'avez pas besoin de les ajouter ici. Pour les recherches à partir de vos applications, les clients de téléchargement qui y sont configurés sont utilisés à la place.", - "IndexerMTeamTpSettingsApiKeyHelpText": "Clé API du site (trouvée dans le panneau de configuration utilisateur => Sécurité => Laboratoire)", + "IndexerMTeamTpSettingsApiKeyHelpText": "Clé API du site (trouvée dans le panneau de configuration utilisateur => Laboratoire)", "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Rechercher uniquement les versions freeleech", "NotificationsTelegramSettingsIncludeAppName": "Inclure {appName} dans le Titre", "NotificationsTelegramSettingsIncludeAppNameHelpText": "Préfixer éventuellement le titre du message par {appName} pour différencier les notifications des différentes applications", @@ -807,5 +807,12 @@ "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Popcorn doré uniquement", "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Rechercher uniquement les versions Golden Popcorn", "IndexerAvistazSettingsUsernameHelpTextWarning": "Seuls les membres de rang et supérieur peuvent utiliser l'API sur cet indexeur.", - "DownloadClientUTorrentProviderMessage": "uTorrent a déjà été connu pour intégrer des mineurs de cryptomonnaie, des logiciels malveillants et des publicités, nous vous recommandons fortement d'utiliser un client différent." + "DownloadClientUTorrentProviderMessage": "uTorrent a déjà été connu pour intégrer des mineurs de cryptomonnaie, des logiciels malveillants et des publicités, nous vous recommandons fortement d'utiliser un client différent.", + "ApplicationAuthPassword": "Mot de passe d'authentification", + "ApplicationAuthUsername": "Utilisateur d'authentification", + "ApplicationAuthPasswordHelpText": "Le mot de passe HTTP Basic Auth utilisé par {appName} pour accéder à cette application. Définir si l'application se trouve derrière un proxy inverse protégé par l'authentification de base.", + "ApplicationAuthUsernameHelpText": "Identifiant HTTP Basic Auth utilisé par {appName} pour accéder à cette application. Définir si l'application se trouve derrière un proxy inverse protégé par l'authentification de base.", + "IndexerIPTorrentsSettingsSearchSortBy": "Tri des résultats", + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Sélectionner l'ordre de tri des résultats de recherche.", + "AllowedHosts": "Hôtes autorisés" } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index e27229c2a93..921962c1e6d 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -706,7 +706,7 @@ "SeedRatio": "Megosztási arány", "SeedTime": "Megosztási idő", "IndexerNewznabSettingsApiKeyHelpText": "Webhely API-kulcsa", - "IndexerNzbIndexSettingsApiKeyHelpText": "Webhely API-kulcsa", + "IndexerNzbIndexSettingsApiKeyHelpText": "Webhely API-kulcsa (opcionális - a keresés működik enélkül is, de egy alacsonyabb rate limittel)", "IndexerPassThePopcornSettingsApiKeyHelpText": "Webhely API-kulcsa", "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Webhely API-kulcsa", "OnGrabHelpText": "A kiadás megragadásakor", @@ -730,5 +730,59 @@ "HistoryDetails": "Előzmény részletei", "IncludeManualGrabsHelpText": "A {appName} alkalmazáson belül indított kézi letöltések belefoglalása", "IndexerAlphaRatioSettingsExcludeScene": "SCENE kizárása", - "IndexerSettingsAppsMinimumSeedersHelpText": "Az alkalmazások által megkövetelt minimális seederszám ahhoz, hogy az indexelő letöltse, üresen hagyva a szinkronizálási profil alapértelmezett értéke lesz érvényes." + "IndexerSettingsAppsMinimumSeedersHelpText": "Az alkalmazások által megkövetelt minimális seederszám ahhoz, hogy az indexelő letöltse, üresen hagyva a szinkronizálási profil alapértelmezett értéke lesz érvényes.", + "ApplicationAuthPassword": "Hitelesítési jelszó", + "ApplicationAuthPasswordHelpText": "A(z) {appName} által használt HTTP Basic Auth jelszó az alkalmazás elérésekor. Akkor állítsd be, ha az alkalmazás Basic Auth hitelesítéssel védett reverse proxy mögött van.", + "ApplicationAuthUsername": "Hitelesítési Felhasználónév", + "ApplicationAuthUsernameHelpText": "A(z) {appName} által használt HTTP Basic Auth jelszó az alkalmazás elérésekor. Akkor állítsd be, ha az alkalmazás Basic Auth hitelesítéssel védett reverse proxy mögött van.", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "A SCENE kiadások kizárása a találatok közül", + "IndexerAvistazSettingsPasswordHelpText": "Jelszó (a weboldalon)", + "IndexerAvistazSettingsPidHelpText": "PID (személyes azonosító) a Fiókom vagy Profilom oldalról", + "IndexerAvistazSettingsUsernameHelpText": "Felhasználónév (a weboldalon)", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Csak tag (member), vagy annál magasabb rangú használhatja az API-t ezen az indexelőn.", + "IndexerBeyondHDSettingsApiKeyHelpText": "API Kulcs a weboldalról (Személyes Biztonság => API Kulcs (vagy My Security => API Key )alatt találod meg", + "IndexerBeyondHDSettingsLimitedOnly": "Csak korlátozott kiadások", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Keresés csak freeleech találatokra (korlátozott feltöltés / Limited UL)", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Keresés csak refund találatokra", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Keresés csak rewind találatokra", + "IndexerBeyondHDSettingsRssKeyHelpText": "RSS kulcs az oldalról (Megtalálható itt: Személyes biztonság (vagy My Security) => RSS kulcs)", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Válaszd ki azokat a kiadási típusokat, amelyek érdekelnek. Ha nincs semmi kiválasztva, az összes lehetőség használva lesz.", + "IndexerCategories": "Indexelő kategóriák", + "IndexerDisabled": "Indexelő letiltva", + "IndexerFailureRate": "Indexelő hibaaránya", + "IndexerFileListSettingsPasskeyHelpText": "Webhely Passkey-je (Ez a betűkből és számokból álló karakterlánc a letöltőkliensedben megjelenő tracker URL-ben)", + "IndexerFileListSettingsUsernameHelpText": "Webhely-felhasználónév", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Rendelkeznie kell User és Torrents jogosultságokkal", + "IndexerGazelleGamesSettingsSearchGroupNames": "Csoportnevek keresése", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Keresés a kiadások között csoportnevek alapján", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Csak a freeleech kiadások megjelenítése", + "IndexerHDBitsSettingsPasskeyHelpText": "Passkey a Felhasználói adatokból", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Jelöld be ezt az opciót, ha a torrent fájlneveit szeretnéd használni kiadási címként", + "IndexerHDBitsSettingsUsernameHelpText": "Felhasználónév a weboldalon", + "IndexerHistoryLoadError": "Hiba az indexelő előzményeinek betöltése közben", + "IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent (sütik User-Agentje)", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "A böngészőből származó sütihez tartozó User-Agent", + "IndexerIPTorrentsSettingsSearchSortBy": "Keresési találatok rendezése", + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Válaszd ki a keresési eredmények rendezési sorrendjét", + "IndexerNebulanceSettingsApiKeyHelpText": "API-kulcs a Felhasználói beállítások (User Settings) > API-kulcsok (Api Keys) menüpontból. A kulcsnak List és Download jogosultságokkal kell rendelkeznie", + "IndexerNewznabSettingsVipExpirationHelpText": "Add meg a VIP lejáratának dátumát (éééé-hh-nn), vagy hagyd üresen; a(z) {appName} 1 héttel a VIP lejárata előtt értesítést küld", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Csak Golden Popcorn", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Csak Golden Popcorn kiadások keresése", + "IndexerSettingsBaseUrlHelpText": "Válaszd ki, hogy a(z) {appName} melyik alap URL-t használja az oldalra irányuló kérésekhez", + "IndexerSettingsCookieHelpText": "Webhely sütije", + "IndexerSettingsFreeleechOnly": "Csak Freeleech", + "IndexerSettingsGrabLimit": "Letöltési (grab) Korlát", + "IndexerSettingsGrabLimitHelpText": "A letöltések maximális száma az adott egység szerint, amelyet a(z) {appName} engedélyez az oldalon", + "IndexerSettingsLimitsUnit": "Korlát egysége", + "IndexerSettingsLimitsUnitHelpText": "Az indexelőnkénti korlátok számításához használt időegység", + "IndexerSettingsPackSeedTime": "Pack seedelési idő", + "IndexerSettingsPackSeedTimeIndexerHelpText": "Az az idő, ameddig egy Pack (évad vagy diszkográfia) torrentet seedelni kell a leállítás előtt; üresen hagyva az alkalmazás alapértelmezett értéke érvényesül", + "IndexerSettingsPasskey": "Pass Key", + "PackSeedTime": "Pack seedelési idő", + "PackSeedTimeHelpText": "Az az idő, ameddig egy Pack (évad vagy diszkográfia) torrentet seedelni kell a leállítás előtt; üresen hagyva az alkalmazás alapértelmezett értéke érvényesül", + "AllowedHosts": "Engedélyezett hosztok", + "AllowedHostsHelpText": "A {appName} által elfogadott hosztnevek és IP-címek vesszővel elválasztott listája. Az aldomainek helyettesítésére használja a „*.” karaktert. Ha üresen hagyja, akkor minden hosztot engedélyez.", + "AllowedHostsNotConfiguredHealthCheckMessage": "A „Megengedett hosztok” beállítás nincs konfigurálva, ezért a {appName} bármely hosztnévhez érkező kérést elfogadja. Az „Általános” beállítások között állítsa be a „Megengedett hosztok” beállítást, hogy korlátozza, mely hosztnevekről érhető el a {appName}.", + "TrustedNetworks": "Megbízható hálózatok", + "TrustedNetworksHelpText": "A megbízható fordított proxy-k által használt IP-címek vagy hálózatok CIDR-jelölésben megadott, vesszővel elválasztott listája. Például: 172.17.0.1, 10.0.0.0/8 vagy fc00::/7. Csak olyan proxy-kat vegyen fel a listára, amelyek megfelelően vannak beállítva a helyes fejlécek küldésére." } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index d08004783fe..254ff6abdfc 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -14,7 +14,7 @@ "Indexer": "Indice", "HideAdvanced": "Nascondi Avanzate", "Health": "Salute", - "Grabbed": "Preso", + "Grabbed": "Prelevato", "Clear": "Cancella", "AppDataLocationHealthCheckMessage": "L'aggiornamento non sarà possibile per evitare la cancellazione di AppData durante l'aggiornamento", "Analytics": "Statistiche", @@ -99,7 +99,7 @@ "BackupIntervalHelpText": "Intervallo fra i backup automatici", "BackupFolderHelpText": "I percorsi relativi saranno nella cartella AppData di {appName}", "Automatic": "Automatico", - "AuthenticationMethodHelpText": "Utilizza nome utente e password per accedere a {appName}", + "AuthenticationMethodHelpText": "Richiede username e password per accedere a {appName}", "Authentication": "Autenticazione", "ApplyTags": "Applica Etichette", "Apply": "Applica", @@ -140,12 +140,12 @@ "DeleteDownloadClient": "Cancella Client di Download", "DeleteBackup": "Cancella Backup", "DatabaseMigration": "Migrazione Database", - "ConnectSettings": "Impostazioni Collegamento", + "ConnectSettings": "Impostazioni Connessioni", "ConnectionLost": "Connessione Persa", "Component": "Componente", "Columns": "Colonne", - "DeleteBackupMessageText": "Sei sicuro di voler eliminare il backup '{name}'?", - "CancelPendingTask": "Sei sicuro di voler cancellare questa operazione in sospeso?", + "DeleteBackupMessageText": "Vuoi eliminare il backup '{name}'?", + "CancelPendingTask": "Vuoi annullare questa operazione in sospeso?", "BranchUpdateMechanism": "Ramo utilizzato dal sistema di aggiornamento esterno", "BranchUpdate": "Ramo da usare per aggiornare {appName}", "AddingTag": "Aggiungendo etichetta", @@ -159,11 +159,11 @@ "IndexerFlags": "Etichette dell'Indice", "EnableSSL": "Abilita SSL", "SettingsLongDateFormat": "Formato Data Esteso", - "SettingsEnableColorImpairedMode": "Abilità la Modalità Daltonica", + "SettingsEnableColorImpairedMode": "Abilita la Modalità Daltonica", "SendAnonymousUsageData": "Invia dati anonimi sull'uso", "ScriptPath": "Percorso dello script", "RssIsNotSupportedWithThisIndexer": "RSS non supportato con questo indice", - "Retention": "Ritenzione", + "Retention": "Conservazione", "Result": "Risultato", "Restore": "Ripristina", "RestartRequiredHelpTextWarning": "Richiede il riavvio per avere effetto", @@ -178,9 +178,9 @@ "ReadTheWikiForMoreInformation": "Leggi la Wiki per più informazioni", "ProwlarrSupportsAnyIndexer": "{appName} supporta molti indici oltre a qualsiasi indice che utilizza gli standard Newznab/Torznab utilizzando \"Newznab Generico\" (per usenet) o \"Torznab Generico\" (per torrent). Cerca e seleziona il tuo indice da qua sotto.", "ProwlarrSupportsAnyDownloadClient": "{appName} supporta qualunque client di download elencato sotto.", - "ProxyUsernameHelpText": "Devi inserire nome utente e password solo se richiesto. Altrimenti lascia vuoto.", + "ProxyUsernameHelpText": "Devi inserire nome utente e password solo se richiesti. Altrimenti lasciali vuoti.", "ProxyType": "Tipo Proxy", - "ProxyPasswordHelpText": "Devi inserire nome utente e password solo se richiesto. Altrimenti lascia vuoto.", + "ProxyPasswordHelpText": "Devi inserire nome utente e password solo se richiesti. Altrimenti lasciali vuoti.", "ProxyBypassFilterHelpText": "Usa ',' come separatore, e '*.' come wildcard per i sottodomini", "PortNumber": "Numero Porta", "Port": "Porta", @@ -216,15 +216,15 @@ "EnableSslHelpText": " Richiede il riavvio come amministratore per avere effetto", "EnableInteractiveSearchHelpText": "Verrà usato durante la ricerca interattiva", "EnableInteractiveSearch": "Abilita la Ricerca Interattiva", - "EnableAutomaticSearchHelpText": "Sarà usata quando la ricerca automatica è eseguita dalla l'intrfaccia o da {appName}", + "EnableAutomaticSearchHelpText": "Sarà usata quando la ricerca automatica è eseguita dall'interfaccia o da {appName}", "EnableAutomaticSearch": "Attiva la Ricerca Automatica", - "DeleteTagMessageText": "Sei sicuro di voler eliminare l'etichetta '{label}'?", - "DeleteNotificationMessageText": "Sei sicuro di voler eliminare la notifica '{name}'?", - "DeleteDownloadClientMessageText": "Sei sicuro di voler eliminare il client di download '{name}'?", + "DeleteTagMessageText": "Vuoi eliminare il tag '{label}'?", + "DeleteNotificationMessageText": "Vuoi eliminare la notifica '{name}'?", + "DeleteDownloadClientMessageText": "Vuoi eliminare il client di download '{name}'?", "BeforeUpdate": "Prima dell'aggiornamento", "Usenet": "Usenet", "Uptime": "Tempo di attività", - "YesCancel": "Sì, Cancella", + "YesCancel": "Sì, annulla", "Version": "Versione", "Username": "Nome Utente", "UseProxy": "Usa Proxy", @@ -232,7 +232,7 @@ "URLBase": "Base Url", "UpdateScriptPathHelpText": "Percorso verso uno script personalizzato che prende un pacchetto di aggiornamento estratto e gestisce il resto del processo di aggiornamento", "UpdateMechanismHelpText": "Usa il sistema di aggiornamento incorporato di {appName} o uno script", - "UpdateAutomaticallyHelpText": "Scarica e installa automaticamente gli aggiornamenti. Sarai comunque in grado in installarli da Sistema: Aggiornamenti", + "UpdateAutomaticallyHelpText": "Download e installazione automatica degli aggiornamenti. Potrai comunque installarli dal menu Sistema: Aggiornamenti", "UnsavedChanges": "Cambiamenti Non Salvati", "UnableToLoadUISettings": "Impossibile caricare le impostazioni interfaccia", "UnableToLoadTags": "Impossibile caricare le Etichette", @@ -290,7 +290,7 @@ "Enabled": "Abilitato", "Encoding": "Codifica", "EnableIndexer": "Abilita Indice", - "AddNewIndexer": "Aggiungi nuovo Indexer", + "AddNewIndexer": "Aggiungi nuovo Indice", "IndexerAuth": "Autenticazione dell'Indice", "AddDownloadClient": "Aggiungi Client di Download", "Category": "Categoria", @@ -305,7 +305,7 @@ "NoLinks": "Nessun Collegamento", "Rss": "RSS", "Wiki": "Wiki", - "AllIndexersHiddenDueToFilter": "Tutti gli Indexer sono nascosti a causa del filtro applicato.", + "AllIndexersHiddenDueToFilter": "Tutti gli indici sono nascosti a causa del filtro applicato.", "DeleteApplicationMessageText": "Sei sicuro di voler eliminare l'applicazione '{name}'?", "DeleteIndexerProxyMessageText": "Sei sicuro di voler eliminare il proxy '{name}'?", "Presets": "Preset", @@ -319,7 +319,7 @@ "CouldNotConnectSignalR": "Non ho potuto connettermi a SignalR, l'interfaccia non si aggiornerà", "Custom": "Personalizzato", "FeatureRequests": "Richieste di funzionalità", - "Grabs": "Prendere", + "Grabs": "Prelievi", "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Tutti i Proxy non sono disponibili a causa di errori", "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxy non disponibili a causa di errori: {indexerProxyNames}", "Reddit": "Reddit", @@ -336,13 +336,13 @@ "OnHealthIssue": "Quando c'è un problema", "TestAllIndexers": "Prova tutti gli Indici", "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent fornito dalla app che ha chiamato la API", - "GrabReleases": "Prendi Release(s)", + "GrabReleases": "Preleva Release", "Link": "Collegamenti", "MappedDrivesRunningAsService": "Le unità di rete mappate non sono disponibili eseguendo come servizio di Windows. Vedere le FAQ per maggiori informazioni", "No": "No", "UnableToLoadIndexers": "Impossibile caricare gli Indici", "Yes": "Sì", - "AddIndexerProxy": "Aggiungi proxy dell'Indexer", + "AddIndexerProxy": "Aggiungi proxy dell'Indice", "AudioSearch": "Ricerca Audio", "BookSearch": "Ricerca Libri", "AddRemoveOnly": "Solo Aggiunta e Rimozione", @@ -361,7 +361,7 @@ "Application": "Applicazione", "BookSearchTypes": "Tipi di Ricerca Libri", "AppSettingsSummary": "Applicazioni e impostazioni per configurare come {appName} interagisce con i tuoi programmi PVR", - "Apps": "Le App", + "Apps": "App", "DevelopmentSettings": "Impostazioni di Sviluppo", "RedirectHelpText": "Reindirizza le richieste di download per l'indice e inoltra il recupero direttamente invece di far passare la richiesta tramite {appName}", "IndexerVipExpiredHealthCheckMessage": "I benefici VIP dell'Indice sono scaduti: {indexerNames}", @@ -372,7 +372,7 @@ "SyncLevel": "Livello Sincronizzazione", "IndexerProxy": "Proxy dell'Indice", "Proxies": "Proxy", - "ApplicationsLoadError": "Impossibile careicare la lista applicazioni", + "ApplicationsLoadError": "Impossibile caricare la lista applicazioni", "Website": "Sito", "Privacy": "Privacy", "SettingsIndexerLogging": "Logging dettagliato dell'indice", @@ -394,7 +394,7 @@ "MovieSearchTypes": "Tipi di Ricerca Film", "MusicSearchTypes": "Tipi di Ricerca Musica", "Notification": "Notifica", - "Notifications": "Notifica", + "Notifications": "Notifiche", "NotSupported": "Non Supportato", "Private": "Privato", "Public": "Pubblico", @@ -405,8 +405,8 @@ "SearchType": "Tipo di Ricerca", "SearchTypes": "Tipi di Ricerca", "SemiPrivate": "Semi-Privato", - "SettingsFilterSentryEvents": "Filtrare gli eventi analitici", - "SettingsFilterSentryEventsHelpText": "Filtra gli eventi noti come errore utente dall'essere inviati come Analitica", + "SettingsFilterSentryEvents": "Filtra eventi statistici", + "SettingsFilterSentryEventsHelpText": "Filtra gli eventi causati da errori dell'utente per evitare che vengano inviati come Statistiche", "SettingsLogRotate": "Rotazione Log", "SettingsLogRotateHelpText": "Numero massimo di file di log da tenere salvati nella cartella log", "SettingsLogSql": "Log Sql", @@ -425,7 +425,7 @@ "SettingsSqlLoggingHelpText": "Scrivi a log tutte le query SQL di {appName}", "SyncLevelAddRemove": "Solo aggiunte e rimozioni: quando gli indici vengono aggiunti o rimossi da {appName}, questa applicazione remota verrà aggiornata.", "SyncLevelFull": "Sincronizzazione completa: mantiene gli indici di questa applicazione completamente sincronizzati. Le modifiche agli indici fatte in {appName} sono sincronizzate in questa applicazione. Qualsiasi cambiamento fatto agli indici da remoto all'interno di questa applicazione verrà sovrascritto da {appName} alla prossima sincronizzazione.", - "MinimumSeedersHelpText": "Seeder minimi richiesti dall'applicazione per far sì che l'indice li recupera", + "MinimumSeedersHelpText": "Seeder minimi richiesti dall'applicazione affinché l'indice possa acquisire il torrent", "SyncProfile": "Profilo Sincronizzazione", "SyncProfiles": "Profili di sincronizzazione", "AddSyncProfile": "Aggiungi Profilo di Sincronizzazione", @@ -442,7 +442,7 @@ "Duration": "Durata", "Ended": "Finito", "NextExecution": "Prossima esecuzione", - "GrabTitle": "Ottieni il Titolo", + "GrabTitle": "Titolo Prelievo", "Parameters": "Parametri", "ElapsedTime": "Tempo trascorso", "EnabledRedirected": "Abilitato, Reindirizzato", @@ -460,20 +460,20 @@ "ApplicationURL": "URL Applicazione", "ApplicationUrlHelpText": "L'URL esterno di questa applicazione, incluso http(s)://, porta e URL base", "Episode": "Episodio", - "Genre": "Generi", + "Genre": "Genere", "Publisher": "Editore", "ApplyTagsHelpTextAdd": "Aggiungi: Aggiunge le etichette alla lista esistente di etichette", - "ApplyTagsHelpTextHowToApplyApplications": "Come applicare etichette agli autori selezionati", + "ApplyTagsHelpTextHowToApplyApplications": "Come applicare etichette alle applicazioni selezionate", "ApplyTagsHelpTextHowToApplyIndexers": "Come applicare etichette agli indici selezionati", "CountIndexersSelected": "{count} indice/i selezionato/i", "DeleteSelectedApplicationsMessageText": "Sei sicuro di voler eliminare {count} applicazione(i) selezionata(e)?", - "DeleteSelectedDownloadClientsMessageText": "Sei sicuro di voler eliminare i '{count}' client di download selezionato/i?", + "DeleteSelectedDownloadClientsMessageText": "Vuoi eliminare {count} client di download selezionato/i?", "SelectIndexers": "Seleziona indici", "Track": "Traccia", "Book": "Libro", "Theme": "Tema", "ApplyTagsHelpTextRemove": "Rimuovi: Rimuove le etichette inserite", - "ApplyTagsHelpTextReplace": "Sostituire: Sostituisce le etichette con quelle inserite (non inserire nessuna etichette per eliminarle tutte)", + "ApplyTagsHelpTextReplace": "Sostituisci: Sostituisce le etichette con quelle inserite (non inserire nessuna etichetta per eliminarle tutte)", "DownloadClientPriorityHelpText": "Dai priorità a multipli Client di download. Round-Robin è usato per i client con la stessa priorità.", "DeleteSelectedDownloadClients": "Cancella i Client di Download", "Album": "Album", @@ -482,23 +482,23 @@ "More": "Altro", "Season": "Stagione", "Year": "Anno", - "UpdateAvailableHealthCheckMessage": "Aggiornamento disponibile: {version}", + "UpdateAvailableHealthCheckMessage": "Nuovo aggiornamento disponibile: {version}", "Author": "Autore", "ApplyChanges": "Applica Cambiamenti", "ApiKeyValidationHealthCheckMessage": "Aggiorna la tua chiave API in modo che abbia una lunghezza di almeno {length} caratteri. Puoi farlo dalle impostazioni o dal file di configurazione", "DeleteAppProfileMessageText": "Sicuro di voler cancellare il profilo dell'app '{name}'?", "RecentChanges": "Cambiamenti Recenti", "WhatsNew": "Cosa c'è di nuovo?", - "ConnectionLostReconnect": "{appName} cercherà di connettersi automaticamente, oppure clicca su ricarica qui sotto.", - "ConnectionLostToBackend": "{appName} ha perso la connessione al backend e dovrà essere ricaricato per ripristinare la funzionalità.", + "ConnectionLostReconnect": "{appName} cercherà di connettersi automaticamente, oppure puoi cliccare su ricarica qui sotto.", + "ConnectionLostToBackend": "{appName} ha perso la connessione con il backend e deve essere ricaricato per ripristinare le funzionalità.", "minutes": "Minuti", "AddConnection": "Aggiungi Connessione", - "NotificationStatusAllClientHealthCheckMessage": "Tutte le applicazioni non sono disponibili a causa di errori", - "NotificationStatusSingleClientHealthCheckMessage": "Applicazioni non disponibili a causa di errori: {notificationNames}", + "NotificationStatusAllClientHealthCheckMessage": "Tutte le notifiche non sono disponibili a causa di errori", + "NotificationStatusSingleClientHealthCheckMessage": "Notifiche non disponibili a causa di errori: {notificationNames}", "AuthForm": "Form (Pagina di Login)", "DisabledForLocalAddresses": "Disabilitato per Indirizzi Locali", "None": "Nessuna", - "ResetAPIKeyMessageText": "Sei sicuro di voler reimpostare la tua chiave API?", + "ResetAPIKeyMessageText": "Vuoi reimpostare la tua chiave API?", "AuthBasic": "Base (Popup del Browser)", "EditSelectedDownloadClients": "Modifica i Client di Download Selezionati", "AppUpdated": "{appName} Aggiornato", @@ -506,9 +506,9 @@ "AddConnectionImplementation": "Aggiungi Connessione - {implementationName}", "AddDownloadClientImplementation": "Aggiungi un Client di Download - {implementationName}", "AddIndexerImplementation": "Aggiungi Indice - {implementationName}", - "AuthenticationRequiredUsernameHelpTextWarning": "Inserisci username", + "AuthenticationRequiredUsernameHelpTextWarning": "Inserisci un nuovo nome utente", "AuthenticationMethod": "Metodo di Autenticazione", - "AuthenticationMethodHelpTextWarning": "Selezione un metodo di autenticazione valido", + "AuthenticationMethodHelpTextWarning": "Seleziona un metodo di autenticazione valido", "AuthenticationRequiredPasswordHelpTextWarning": "Inserisci la nuova password", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Conferma la nuova password", "Clone": "Copia", @@ -516,8 +516,8 @@ "AppUpdatedVersion": "{appName} è stato aggiornato alla versione `{version}`, per vedere le modifiche devi ricaricare {appName}", "AddApplicationImplementation": "Aggiungi Connessione - {implementationName}", "AddIndexerProxyImplementation": "Aggiungi Proxy per Indice - {implementationName}", - "EditApplicationImplementation": "Aggiungi Condizione - {implementationName}", - "CountApplicationsSelected": "{count} Collezione(i) Selezionate", + "EditApplicationImplementation": "Modifica Applicazione - {implementationName}", + "CountApplicationsSelected": "{count} applicazione/i selezionata/e", "EditConnectionImplementation": "Modifica Connessione - {implementationName}", "EditDownloadClientImplementation": "Modifica Client di Download - {implementationName}", "EditIndexerImplementation": "Modifica Indice - {implementationName}", @@ -532,7 +532,7 @@ "Directory": "Cartella", "CustomFilter": "Filtro Personalizzato", "IndexerHDBitsSettingsCodecs": "Codec", - "IndexerHDBitsSettingsMediums": "medio", + "IndexerHDBitsSettingsMediums": "Supporti", "GrabRelease": "Preleva Release", "ProxyValidationBadRequest": "Il test del proxy è fallito. Codice Stato: {statusCode}", "Discord": "Discord", @@ -546,7 +546,7 @@ "DownloadClientRTorrentSettingsUrlPath": "Percorso Url", "Default": "Predefinito", "DownloadClientPneumaticSettingsStrmFolder": "Cartella Strm", - "IndexerDisabled": "Indexer Disattivato", + "IndexerDisabled": "Indice Disattivato", "GoToApplication": "Vai all'applicazione", "AreYouSureYouWantToDeleteIndexer": "Sei sicuro di voler eliminare '{name}' da {appName}?", "IndexerStatus": "Stato Indice", @@ -571,7 +571,7 @@ "NoIndexersFound": "Nessun indice trovato", "PasswordConfirmation": "Conferma Password", "NoHistoryFound": "Nessun storico trovato", - "DeleteSelectedIndexersMessageText": "Confermi di voler eliminare i {count} indici selezionati?", + "DeleteSelectedIndexersMessageText": "Vuoi eliminare i {count} indici selezionati?", "UsenetBlackholeNzbFolder": "Cartella Nzb", "VipExpiration": "Scadenza VIP", "OverrideAndAddToDownloadClient": "Sovrascrivi e aggiungi alla coda di download", @@ -635,9 +635,9 @@ "SelectDownloadClientModalTitle": "{modalTitle} - Seleziona Client di Download", "DownloadClientSettingsDestinationHelpText": "Specifica manualmente la destinazione dei download, lascia vuoti per usare la predefinita", "IndexerDownloadClientHealthCheckMessage": "Indici con client di download non validi: {indexerNames}.", - "SeedTimeHelpText": "Il rapporto che un torrent dovrebbe raggiungere prima di essere fermato, vuoto è il predefinito dell'app", + "SeedTimeHelpText": "Il tempo per cui un torrent dovrebbe fare seed prima di fermarsi, vuoto è il predefinito dell'app", "IndexerPassThePopcornSettingsApiKeyHelpText": "API Key Sito", - "IndexerNzbIndexSettingsApiKeyHelpText": "API Key Sito", + "IndexerNzbIndexSettingsApiKeyHelpText": "Chiave API del sito (opzionale — la ricerca funziona senza, ma con un limite di frequenza inferiore)", "IndexerNewznabSettingsApiKeyHelpText": "API Key Sito", "Fixed": "Corretto", "Any": "Qualunque", @@ -661,24 +661,157 @@ "InstallLatest": "Installa il più Recente", "CurrentlyInstalled": "Attualmente Installato", "PreviouslyInstalled": "Precedentemente Installato", - "Mixed": "Fissato", - "ApplicationTagsHelpText": "Sincronizza gli indexer con questa applicazione se hanno uno o più tag corrispondenti. Se non sono elencati tag, nessun indexer verrà escluso dalla sincronizzazione a causa dei propri tag.", - "ApplicationTagsHelpTextWarning": "I tag devono essere usati con cautela, poiché possono avere effetti indesiderati. Un'app con un tag si sincronizzerà solo con gli indexer che hanno lo stesso tag.", + "Mixed": "Misto", + "ApplicationTagsHelpText": "Sincronizza gli indici con questa applicazione se hanno uno o più tag corrispondenti. Se non sono elencati tag, nessun indice verrà escluso dalla sincronizzazione a causa dei propri tag.", + "ApplicationTagsHelpTextWarning": "I tag devono essere usati con cautela, poiché possono avere effetti indesiderati. Un'app con un tag si sincronizzerà solo con gli indici che hanno lo stesso tag.", "AuthQueries": "Query di autenticazione", "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Se un torrent è bloccato tramite hash, potrebbe non essere correttamente rifiutato durante l’uso di RSS/Ricerca con alcuni indexer. Abilitando questa opzione, il torrent verrà rifiutato dopo essere stato acquisito, ma prima di essere inviato al client.", - "AppsMinimumSeedersHelpText": "Numero minimo di seeders richiesti dall’applicazione affinché l’indexer possa acquisire il torrent; se vuoto, viene utilizzato il valore predefinito del profilo di sincronizzazione", - "IndexerSettingsAppsMinimumSeedersHelpText": "Numero minimo di seeders richiesti dall’applicazione affinché l’indexer possa acquisire il torrent; se vuoto, viene utilizzato il valore predefinito del profilo di sincronizzazione", + "AppsMinimumSeedersHelpText": "Numero minimo di seeders richiesti dall'applicazione affinché l'indice possa acquisire il torrent; se vuoto, viene utilizzato il valore predefinito del profilo di sincronizzazione", + "IndexerSettingsAppsMinimumSeedersHelpText": "Numero minimo di seeders richiesti dall'applicazione affinché l'indice possa acquisire il torrent; se vuoto, viene utilizzato il valore predefinito del profilo di sincronizzazione", "TotalHostQueries": "Totale Richieste", "AppsMinimumSeeders": "Seeder Minimi", - "PackSeedTimeHelpText": "Il rapporto che un torrent dovrebbe raggiungere prima di essere fermato, vuoto è il predefinito dell'app", - "IndexerTagsHelpTextWarning": "I tag devono essere usati con cautela, poiché possono avere effetti indesiderati. Un'app con un tag si sincronizzerà solo con gli indexer che hanno lo stesso tag.", - "IndexerBeyondHDSettingsRefundOnly": "Solo Lettura", - "IndexerBeyondHDSettingsRewindOnly": "Solo Lettura", + "PackSeedTimeHelpText": "Il tempo per cui un pack (stagione o discografia) dovrebbe fare seed prima di fermarsi, vuoto è il predefinito dell'app", + "IndexerTagsHelpTextWarning": "I tag devono essere usati con cautela, poiché possono avere effetti indesiderati. Un indice con un tag si sincronizzerà solo con le app che hanno lo stesso tag.", + "IndexerBeyondHDSettingsRefundOnly": "Solo Rimborso", + "IndexerBeyondHDSettingsRewindOnly": "Solo Rewind", "IndexerSettingsAppsMinimumSeeders": "Seeder Minimi", - "IndexerSettingsPackSeedTimeIndexerHelpText": "Il rapporto che un torrent dovrebbe raggiungere prima di essere fermato, vuoto è il predefinito dell'app", - "IndexerSettingsSeedTimeHelpText": "Il rapporto che un torrent dovrebbe raggiungere prima di essere fermato, vuoto è il predefinito dell'app", + "IndexerSettingsPackSeedTimeIndexerHelpText": "Il tempo per cui un pack (stagione o discografia) dovrebbe fare seed prima di fermarsi, vuoto è il predefinito dell'app", + "IndexerSettingsSeedTimeHelpText": "Il tempo per cui un torrent dovrebbe fare seed prima di fermarsi, vuoto è il predefinito dell'app", "LogSizeLimit": "Limite Dimensione", - "TheLogLevelDefault": "Il livello di log predefinito è 'Debug' e può essere modificato nelle [Impostazioni Generali](settings/general)", - "DownloadClientSettingsUrlBaseHelpText": "Aggiunge un prefisso all'url della {connectionName}, come {url}", - "FailedToFetchUpdates": "Impossibile recuperare aggiornamenti" + "TheLogLevelDefault": "Il livello di log predefinito è 'Debug' e può essere modificato nelle [Impostazioni Generali](/settings/general)", + "DownloadClientSettingsUrlBaseHelpText": "Aggiunge un prefisso all'url del {clientName}, come {url}", + "FailedToFetchUpdates": "Impossibile recuperare aggiornamenti", + "LastFailure": "Ultimo Fallimento", + "IncludeManualGrabsHelpText": "Includi i prelievi manuali effettuati in {appName}", + "HealthMessagesInfoBox": "Puoi trovare ulteriori informazioni sulla causa di questi messaggi di controllo salute cliccando sul link wiki (icona libro) alla fine della riga, oppure controllando i tuoi [log]({link}). Se hai difficoltà a interpretarli puoi contattare il nostro supporto, ai link qui sotto.", + "UpdaterLogFiles": "File di Log Aggiornamento", + "StopSelecting": "Interrompi Selezione", + "QueryType": "Tipo Query", + "LogSizeLimitHelpText": "Dimensione massima del file di log in MB prima dell'archiviazione. Il valore predefinito è 1 MB.", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Posizione opzionale in cui salvare i download, lascia vuoto per usare la posizione predefinita di Transmission", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Aggiunge un prefisso all'url rpc di {clientName}, es. {url}, predefinito '{defaultUrl}'", + "DownloadClientUTorrentProviderMessage": "uTorrent ha una storia di inclusione di cryptominer, malware e pubblicità; ti consigliamo vivamente di scegliere un client diverso.", + "ClickToChangeQueryOptions": "Clicca per modificare le opzioni della query", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Prefissa opzionalmente il titolo del messaggio con {appName} per distinguere le notifiche da applicazioni diverse", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Percorso all'endpoint XMLRPC, vedi {url}. Di solito è RPC2 o [percorso ruTorrent]{url2} con ruTorrent.", + "Logout": "Esci", + "IndexerDownloadClientHelpText": "Specifica quale client di download viene usato per i prelievi effettuati in {appName} da questo indice", + "IndexerFailureRate": "Tasso di Fallimento dell'Indice", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Cerca solo release freeleech", + "IndexerFileListSettingsPasskeyHelpText": "Passkey del sito (la stringa alfanumerica nell'url del tracker mostrata nel client di download)", + "IndexerFileListSettingsUsernameHelpText": "Nome utente del sito", + "ProwlarrDownloadClientsAlert": "Se intendi fare ricerche direttamente in {appName}, devi aggiungere client di download. Altrimenti non è necessario aggiungerli qui. Per le ricerche dalle tue app, vengono usati i client di download configurati lì.", + "ProwlarrDownloadClientsInAppOnlyAlert": "I client di download sono solo per le ricerche in-app di {appName} e non si sincronizzano con le app. Non ci sono piani per aggiungere tale funzionalità.", + "Open": "Apri", + "IndexerPassThePopcornSettingsApiUserHelpText": "Queste impostazioni si trovano nelle impostazioni di sicurezza di PassThePopcorn (Modifica Profilo > Sicurezza).", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Cerca solo release freeleech", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Solo Golden Popcorn", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Cerca solo release Golden Popcorn", + "IndexerHDBitsSettingsPasskeyHelpText": "Passkey dai Dettagli Utente", + "IndexerHDBitsSettingsUseFilenames": "Usa Nomi File", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Seleziona questa opzione per usare i nomi file dei torrent come titoli delle release", + "IndexerHDBitsSettingsUsernameHelpText": "Nome utente del sito", + "PreferMagnetUrl": "Preferisci URL Magnet", + "PreferMagnetUrlHelpText": "Se abilitato, questo indice preferirà l'uso di URL magnet per i prelievi, con fallback ai link torrent", + "OnGrabHelpText": "Al Prelievo della Release", + "IndexerAlphaRatioSettingsExcludeScene": "Escludi SCENE", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Escludi le release SCENE dai risultati", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Cerca solo release freeleech", + "IndexerMTeamTpSettingsApiKeyHelpText": "Chiave API del sito (trovata nel Pannello di Controllo Utente => Laboratory)", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Cerca solo release freeleech", + "IndexerNewznabSettingsVipExpirationHelpText": "Inserisci la data (aaaa-mm-gg) di scadenza VIP o lascia vuoto; {appName} notificherà 1 settimana prima della scadenza VIP", + "IndexerRedactedSettingsApiKeyHelpText": "Chiave API del sito (trovata in Impostazioni => Impostazioni Accesso)", + "ProxyValidationUnableToConnect": "Impossibile connettersi al proxy: {exceptionMessage}. Controlla il log intorno a questo errore per i dettagli", + "SelectedCountOfCountReleases": "{selectedCount} di {itemCount} release selezionate", + "InstallMajorVersionUpdate": "Installa Aggiornamento", + "InstallMajorVersionUpdateMessage": "Questo aggiornamento installerà una nuova versione principale e potrebbe non essere compatibile con il tuo sistema. Vuoi installare questo aggiornamento?", + "InstallMajorVersionUpdateMessageLink": "Controlla [{domain}]({url}) per ulteriori informazioni.", + "ApplicationAuthPassword": "Password", + "ApplicationAuthPasswordHelpText": "Password HTTP Basic Auth usata da {appName} per accedere a questa applicazione. Da impostare se l'applicazione si trova dietro un reverse proxy protetto da Basic Auth.", + "ApplicationAuthUsername": "Nome utente", + "ApplicationAuthUsernameHelpText": "Nome utente HTTP Basic Auth usato da {appName} per accedere a questa applicazione. Da impostare se l'applicazione si trova dietro un reverse proxy protetto da Basic Auth.", + "DownloadClientAriaSettingsDirectoryHelpText": "Posizione opzionale in cui salvare i download, lascia vuoto per usare la posizione predefinita di Aria2", + "ManualGrab": "Prelievo Manuale", + "RepeatSearch": "Ripeti Ricerca", + "OnHealthRestored": "Salute Ripristinata", + "OnHealthRestoredHelpText": "Salute Ripristinata", + "TotalUserAgentGrabs": "Totale Prelievi User Agent", + "TotalUserAgentQueries": "Totale Query User Agent", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "I file .strm in questa cartella verranno importati da drone", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Cerca solo release rewind", + "IndexerBeyondHDSettingsRssKeyHelpText": "Chiave RSS del sito (trovata in La Mia Sicurezza => Chiave RSS)", + "IndexerGazelleGamesSettingsApiKeyHelpText": "Chiave API del sito (trovata in Impostazioni => Impostazioni Accesso)", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Deve avere i permessi User e Torrents", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Cerca solo release freeleech", + "IndexerGazelleGamesSettingsSearchGroupNames": "Cerca Nomi Gruppo", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Cerca release per nome del gruppo", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Mostra solo release freeleech", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Aggiunge proprietà del media come tag. I suggerimenti sono esempi.", + "DownloadClientFloodSettingsTagsHelpText": "Tag iniziali di un download. Per essere riconosciuto, un download deve avere tutti i tag iniziali. Evita conflitti con download non correlati.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Aggiunge un prefisso all'API di Flood, come {url}", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Definisci l'URL base dell'API Freebox con la versione API, es. '{url}', predefinito '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID App fornito durante la creazione dell'accesso all'API Freebox (es. 'app_id')", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Se abilitato, aggiunge torrent e magnet a rTorrent in stato fermato. Potrebbe compromettere i file magnet.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Posizione opzionale in cui salvare i download, lascia vuoto per usare la posizione predefinita di rTorrent", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Cartella condivisa opzionale in cui salvare i download, lascia vuoto per usare la posizione predefinita di Download Station", + "NotificationsEmailSettingsUseEncryptionHelpText": "Se preferire la crittografia se configurata sul server, usare sempre la crittografia via SSL (solo porta 465) o StartTLS (qualsiasi altra porta) o non usare mai la crittografia", + "TorrentBlackholeSaveMagnetFiles": "Salva File Magnet", + "TorrentBlackholeSaveMagnetFilesExtension": "Estensione File Magnet", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Estensione da usare per i link magnet, predefinita '.magnet'", + "TorrentBlackholeSaveMagnetFilesHelpText": "Salva il link magnet se non è disponibile un file .torrent (utile solo se il client di download supporta i magnet salvati come file)", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Seleziona i tipi di release di interesse. Se nessuno è selezionato, vengono usate tutte le opzioni.", + "AverageGrabs": "Media Prelievi", + "AverageQueries": "Media Richieste", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "User-Agent associato al cookie usato dal browser", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Cerca solo release freeleech", + "IndexerIPTorrentsSettingsSearchSortBy": "Ordina Risultati Per", + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Seleziona l'ordine di classificazione dei risultati di ricerca", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token App ottenuto durante la creazione dell'accesso all'API Freebox (es. 'app_token')", + "DownloadClientFreeboxSettingsHostHelpText": "Nome host o indirizzo IP del Freebox, predefinito '{url}' (funziona solo sulla stessa rete)", + "DownloadClientFreeboxSettingsPortHelpText": "Porta usata per accedere all'interfaccia Freebox, predefinito '{port}'", + "TotalIndexerQueries": "Totale Query Indice", + "TotalIndexerSuccessfulGrabs": "Totale Prelievi Riusciti Indice", + "IndexerSettingsPasskey": "Passkey", + "IndexerSettingsPreferMagnetUrl": "Preferisci URL Magnet", + "IndexerSettingsPreferMagnetUrlHelpText": "Se abilitato, questo indice preferirà l'uso di URL magnet per i prelievi, con fallback ai link torrent", + "IndexerSettingsQueryLimit": "Limite Query", + "IndexerSettingsQueryLimitHelpText": "Numero massimo di query nell'unità di tempo specificata che {appName} consentirà al sito", + "DownloadClientQbittorrentSettingsContentLayout": "Layout Contenuto", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Se usare il layout contenuto configurato in qBittorrent, il layout originale del torrent o creare sempre una sottocartella (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Prima e Ultima Prima", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Scarica prima i pezzi iniziali e finali (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Stato iniziale per i torrent aggiunti a qBittorrent. I torrent forzati non rispettano le restrizioni di seed", + "IndexerNebulanceSettingsApiKeyHelpText": "Chiave API dalle Impostazioni Utente > Chiavi Api. La chiave deve avere i permessi List e Download", + "DownloadClientSettingsDefaultCategoryHelpText": "Categoria di riserva predefinita se non esiste una categoria mappata per una release. Aggiungere una categoria specifica per {appName} evita conflitti con download non correlati a {appName}. Usare una categoria è opzionale ma fortemente consigliato.", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Categoria di riserva predefinita se non esiste una categoria mappata per una release. Aggiungere una categoria specifica per {appName} evita conflitti con download non correlati a {appName}. Usare una categoria è opzionale ma fortemente consigliato. Crea una sottocartella [categoria] nella cartella di output.", + "SeedTime": "Tempo Seed", + "IndexerOrpheusSettingsApiKeyHelpText": "Chiave API del sito (trovata in Impostazioni => Impostazioni Accesso)", + "InitialFailure": "Fallimento Iniziale", + "Install": "Installa", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Rifiuta Hash Torrent in Blocklist durante il Prelievo", + "TotalGrabs": "Totale Prelievi", + "TotalHostGrabs": "Totale Prelievi Host", + "OverrideGrabModalTitle": "Sovrascrivi e Preleva - {title}", + "PackSeedTime": "Tempo Seed Pack", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Cerca solo release freeleech", + "IndexerAvistazSettingsPasswordHelpText": "Password del sito", + "IndexerAvistazSettingsPidHelpText": "PID dalla pagina Il Mio Account o Il Mio Profilo", + "IndexerAvistazSettingsUsernameHelpText": "Nome utente del sito", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Solo i membri con rango member o superiore possono usare l'API su questo indicizzatore.", + "IndexerBeyondHDSettingsApiKeyHelpText": "Chiave API del sito (trovata in La Mia Sicurezza => Chiave API)", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Cerca solo release freeleech", + "IndexerBeyondHDSettingsLimitedOnly": "Solo Limitato", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Cerca solo freeleech (Limited UL)", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Cerca solo release con rimborso", + "IndexerSettingsCookieHelpText": "Cookie del sito", + "IndexerSettingsFreeleechOnly": "Solo Freeleech", + "IndexerSettingsGrabLimit": "Limite Prelievi", + "IndexerSettingsGrabLimitHelpText": "Numero massimo di prelievi nell'unità di tempo specificata che {appName} consentirà al sito", + "IndexerSettingsLimitsUnit": "Unità Limite", + "IndexerSettingsLimitsUnitHelpText": "Unità di tempo per il conteggio dei limiti per indice", + "IndexerSettingsPackSeedTime": "Tempo Seed Pack", + "IndexerSettingsSeedRatioHelpText": "Il rapporto che un torrent dovrebbe raggiungere prima di fermarsi, vuoto usa il predefinito del client di download. Il rapporto deve essere almeno 1.0 e seguire le regole dell'indice", + "IndexerSettingsSeedTime": "Tempo Seed", + "DownloadClientSettingsPriorityItemHelpText": "Priorità da usare durante il prelievo", + "FailedToFetchSettings": "Impossibile recuperare le impostazioni" } diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index e729d0c6fa0..a00c1fda460 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -2,9 +2,9 @@ "Add": "Legg til", "AddDownloadClient": "Legg til nedlastingsklient", "Added": "La til", - "AddingTag": "Legger til tag", + "AddingTag": "Legger til merke", "All": "Alle", - "Analytics": "Analyse", + "Analytics": "Statistikk", "AppDataDirectory": "AppData Katalog", "ApplyTags": "Bruk Tags", "Authentication": "Godkjenning", @@ -26,8 +26,8 @@ "AddDownloadClientToProwlarr": "Ved å legge til en nedlastingsklient kan {appName} sende utgivelser direkte fra brukergrensesnittet mens du gjør et manuelt søk.", "AddIndexer": "Legg til indekser", "Age": "Alder", - "ApiKey": "API Nøkkel", - "AppDataLocationHealthCheckMessage": "Oppdatering vil ikke være mulig for å forhindre sletting av AppData på oppdateringen", + "ApiKey": "API-nøkkel", + "AppDataLocationHealthCheckMessage": "Oppdatering vil ikke være mulig for å forhindre sletting av AppData ved oppdatering", "Apply": "Bruk", "AutomaticSearch": "Automatisk Søk", "BackupRetentionHelpText": "Automatiske sikkerhetskopier som er eldre enn oppbevaringsperioden, blir ryddet opp automatisk", diff --git a/src/NzbDrone.Core/Localization/Core/pl.json b/src/NzbDrone.Core/Localization/Core/pl.json index 0c3263030ae..362ddf860df 100644 --- a/src/NzbDrone.Core/Localization/Core/pl.json +++ b/src/NzbDrone.Core/Localization/Core/pl.json @@ -1,6 +1,6 @@ { "Clear": "Wyczyść", - "BackupNow": "Utwórz kopię teraz", + "BackupNow": "Zrób kopię zapasową teraz", "Backup": "Kopia zapasowa", "AppDataLocationHealthCheckMessage": "Aktualizacja nie będzie możliwa, aby zapobiec usunięciu AppData podczas aktualizacji", "Analytics": "Analityka", @@ -16,18 +16,18 @@ "SSLCertPasswordHelpText": "Hasło do pliku pfx", "TableOptionsColumnsMessage": "Wybierz, które kolumny są widoczne i w jakiej kolejności się pojawiają", "BeforeUpdate": "Przed aktualizacją", - "BindAddress": "Adres nasłuchu", + "BindAddress": "Adres powiązania", "BranchUpdate": "Gałąź używana do aktualizacji {appName}", "Cancel": "Anuluj", - "Automatic": "Automatycznie", + "Automatic": "Automatyczny", "ApplyTags": "Zastosuj tagi", - "BackupIntervalHelpText": "Interwał między automatycznymi kopiami zapasowymi", + "BackupIntervalHelpText": "Odstęp czasu między automatycznymi kopiami zapasowymi", "Close": "Zamknij", "CloseCurrentModal": "Zamknij bieżący tryb", "Delete": "Usuń", "Discord": "Discord", "Edit": "Edytuj", - "EnableAutomaticSearchHelpText": "Będzie używane, gdy automatyczne wyszukiwania są wykonywane przez UI lub przez {appName}", + "EnableAutomaticSearchHelpText": "Będzie używany, gdy automatyczne wyszukiwania są wykonywane przez interfejs użytkownika lub przez {appName}", "KeyboardShortcuts": "Skróty klawiaturowe", "UnableToAddANewAppProfilePleaseTryAgain": "Nie można dodać nowego profilu jakości, spróbuj ponownie.", "URLBase": "Baza adresów URL", @@ -63,10 +63,10 @@ "Exception": "Wyjątek", "ExistingTag": "Istniejący tag", "Filename": "Nazwa pliku", - "NoChange": "Bez zmian", + "NoChange": "Bez zmiany", "Add": "Dodaj", "AppDataDirectory": "Katalog AppData", - "AutomaticSearch": "Wyszukiwanie automatyczne", + "AutomaticSearch": "Automatyczne wyszukiwanie", "Branch": "Gałąź", "Connections": "Połączenia", "ConnectSettings": "Ustawienia połączeń", @@ -78,7 +78,7 @@ "Level": "Poziom", "LogFiles": "Pliki logów", "Mechanism": "Mechanizm", - "NoChanges": "Brak zmian", + "NoChanges": "Bez zmian", "NoUpdatesAreAvailable": "Brak dostępnych aktualizacji", "OAuthPopupMessage": "Twoja przeglądarka blokuje wyskakujące okienka", "Ok": "Ok", @@ -101,19 +101,19 @@ "ErrorLoadingContents": "Błąd ładowania zawartości", "Events": "Zdarzenia", "SettingsLongDateFormat": "Format długiej daty", - "AnalyticsEnabledHelpText": "Wysyłaj anonimowe dane o użyciu i błędach na serwery {appName}. Obejmuje to informacje o przeglądarce, używanych stronach WebUI {appName}, raportach błędów oraz wersji systemu i środowiska uruchomieniowego. Użyjemy tych informacji do priorytetyzacji funkcji i poprawek błędów.", + "AnalyticsEnabledHelpText": "Wysyłaj anonimowe informacje o użytkowaniu i błędach do serwerów {appName}. Obejmuje to informacje o Twojej przeglądarce, których stron interfejsu webowego {appName} używasz, raportowanie błędów, a także wersję systemu operacyjnego i środowiska wykonawczego. Wykorzystamy te informacje, aby nadać priorytet funkcjom i poprawkom błędów.", "ApplicationStatusCheckAllClientMessage": "Wszystkie aplikacje są niedostępne z powodu błędów", "ApplicationStatusCheckSingleClientMessage": "Aplikacje niedostępne z powodu błędów: {0}", "Apply": "Zastosuj", "Authentication": "Uwierzytelnianie", "AuthenticationMethodHelpText": "Wymagaj nazwy użytkownika i hasła, aby uzyskać dostęp do {appName}", - "BackupFolderHelpText": "Ścieżki względne będą w katalogu AppData aplikacji {appName}", - "BackupRetentionHelpText": "Automatyczne kopie zapasowe starsze niż okres przechowywania będą automatycznie czyszczone", + "BackupFolderHelpText": "Względne ścieżki będą znajdować się w katalogu AppData {appName}", + "BackupRetentionHelpText": "Automatyczne kopie zapasowe starsze niż okres przechowywania zostaną automatycznie wyczyszczone", "BindAddressHelpText": "Prawidłowy adres IP, localhost lub '*' dla wszystkich interfejsów", "BranchUpdateMechanism": "Gałąź używana przez zewnętrzny mechanizm aktualizacji", "BypassProxyForLocalAddresses": "Pomiń proxy dla adresów lokalnych", "CancelPendingTask": "Czy na pewno chcesz anulować to oczekujące zadanie?", - "CertificateValidation": "Weryfikacja certyfikatu", + "CertificateValidation": "Walidacja certyfikatu", "ClientPriority": "Priorytet klienta", "CloneProfile": "Klonuj profil", "Component": "Komponent", @@ -133,7 +133,7 @@ "DownloadClientStatusSingleClientHealthCheckMessage": "Klienci pobierania niedostępni z powodu błędów: {downloadClientNames}", "EditIndexer": "Edytuj indeksator", "Enable": "Włącz", - "EnableAutomaticSearch": "Włącz wyszukiwanie automatyczne", + "EnableAutomaticSearch": "Włącz automatyczne wyszukiwanie", "Enabled": "Włączone", "DownloadClientStatusAllClientHealthCheckMessage": "Wszyscy klienci pobierania są niedostępni z powodu błędów", "EnableInteractiveSearch": "Włącz wyszukiwanie interaktywne", @@ -142,20 +142,20 @@ "FeatureRequests": "Propozycje funkcji", "Files": "Pliki", "Filter": "Filtr", - "Fixed": "Stałe", + "Fixed": "Naprawione", "FocusSearchBox": "Zaznacz pole wyszukiwania", "Folder": "Folder", "ForMoreInformationOnTheIndividualDownloadClients": "Aby uzyskać więcej informacji na temat poszczególnych klientów pobierania, kliknij przyciski informacyjne.", "General": "Ogólne", "GeneralSettings": "Ustawienia ogólne", "GeneralSettingsSummary": "Port, SSL, nazwa użytkownika / hasło, proxy, analizy i aktualizacje", - "Grabbed": "Wybrane", + "Grabbed": "Pobrane", "NoIssuesWithYourConfiguration": "Brak problemów z konfiguracją", "HideAdvanced": "Ukryj zaawansowane", "History": "Historia", "Hostname": "Nazwa hosta", "IgnoredAddresses": "Ignorowane adresy", - "IllRestartLater": "Zrestartuję później", + "IllRestartLater": "Uruchomię ponownie później", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Wszystkie indeksery są niedostępne z powodu błędów przez ponad 6 godzin", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indeksery niedostępne z powodu błędów przez ponad 6 godzin: {indexerNames}", "IndexerPriority": "Priorytet indeksera", @@ -167,7 +167,7 @@ "Interval": "Interwał", "Language": "Język", "LogLevel": "Poziom logowania", - "LogLevelTraceHelpTextWarning": "Logowanie Trace powinno być włączane tylko tymczasowo", + "LogLevelTraceHelpTextWarning": "Rejestrowanie śledzenia powinno być włączone tylko tymczasowo", "Logs": "Logi", "Manual": "Ręcznie", "MovieIndexScrollBottom": "Indeks filmów: przewiń w dół", @@ -258,11 +258,11 @@ "UISettings": "Ustawienia interfejsu użytkownika", "UnableToAddANewApplicationPleaseTryAgain": "Nie można dodać nowego powiadomienia, spróbuj ponownie.", "UnableToAddANewDownloadClientPleaseTryAgain": "Nie można dodać nowego klienta pobierania, spróbuj ponownie.", - "UnableToAddANewIndexerPleaseTryAgain": "Nie można dodać nowego indeksatora, spróbuj ponownie.", + "UnableToAddANewIndexerPleaseTryAgain": "Nie można dodać nowego indeksera, spróbuj ponownie.", "UnableToAddANewIndexerProxyPleaseTryAgain": "Nie można dodać nowego indeksatora, spróbuj ponownie.", - "UnableToAddANewNotificationPleaseTryAgain": "Nie można dodać nowego powiadomienia, spróbuj ponownie.", - "BackupsLoadError": "Nie można wczytać kopii zapasowych", - "DownloadClientsLoadError": "Nie można wczytać klientów pobierania", + "UnableToAddANewNotificationPleaseTryAgain": "Nie udało się dodać nowego powiadomienia, spróbuj ponownie.", + "BackupsLoadError": "Nie można załadować kopii zapasowych", + "DownloadClientsLoadError": "Nie można załadować klientów pobierania", "UnableToLoadGeneralSettings": "Nie można załadować ustawień ogólnych", "UnableToLoadHistory": "Nie można załadować historii", "UnableToLoadTags": "Nie można załadować tagów", @@ -346,21 +346,21 @@ "ApplicationUrlHelpText": "Zewnętrzny URL tej aplikacji, w tym http(s)://, port i bazowy URL", "ApplyTagsHelpTextAdd": "Dodaj: Dodaj tagi do istniejącej listy tagów", "ApplyTagsHelpTextHowToApplyApplications": "Jak zastosować tagi do wybranych aplikacji", - "ApplyTagsHelpTextHowToApplyIndexers": "Jak zastosować tagi do wybranych indekserów", + "ApplyTagsHelpTextHowToApplyIndexers": "Jak stosować tagi do wybranych indekserów", "DeleteSelectedApplicationsMessageText": "Czy na pewno chcesz usunąć {count} wybranych aplikacji?", "DeleteSelectedDownloadClients": "Usuń klienta(ów) pobierania", "DeleteSelectedDownloadClientsMessageText": "Czy na pewno chcesz usunąć {count} zaznaczonych klientów pobierania?", "Year": "Rok", "More": "Więcej", "ApplyTagsHelpTextRemove": "Usuń: Usuń wprowadzone tagi", - "ApplyTagsHelpTextReplace": "Zastąp: Zastąp tagi wprowadzonymi tagami (nie wpisuj tagów, aby wyczyścić wszystkie)", + "ApplyTagsHelpTextReplace": "Zastąp: Zastąp tagi wprowadzonymi tagami (nie wprowadzaj tagów, aby usunąć wszystkie tagi)", "DeleteSelectedIndexersMessageText": "Czy na pewno chcesz usunąć {count} zaznaczonych indekserów?", "DownloadClientPriorityHelpText": "Nadaj priorytet wielu klientom pobierania. W przypadku klientów o tym samym priorytecie używany jest system kołowy (\"każdy na każdego\").", "Track": "Ślad", "UpdateAvailableHealthCheckMessage": "Dostępna jest nowa aktualizacja: {version}", "Genre": "Gatunki", "ApplyChanges": "Zastosuj zmiany", - "ApiKeyValidationHealthCheckMessage": "Zaktualizuj klucz API tak, aby miał co najmniej {length} znaków. Możesz to zrobić w ustawieniach lub pliku konfiguracyjnym", + "ApiKeyValidationHealthCheckMessage": "Zaktualizuj swój klucz API aby był długi na co najmniej {length} znaków. Możesz to zrobić poprzez ustawienia lub plik konfiguracyjny", "DeleteAppProfileMessageText": "Czy na pewno chcesz usunąć profil aplikacji '{name}'?", "ConnectionLostReconnect": "{appName} spróbuje połączyć się automatycznie lub możesz kliknąć przeładuj poniżej.", "RecentChanges": "Ostatnie zmiany", @@ -372,7 +372,7 @@ "NotificationStatusAllClientHealthCheckMessage": "Wszystkie powiadomienia są niedostępne z powodu błędów", "AuthForm": "Formularze (strona logowania)", "DisabledForLocalAddresses": "Wyłączone dla adresów lokalnych", - "None": "Brak", + "None": "Żaden", "AuthBasic": "Podstawowe (wyskakujące okienko przeglądarki)", "ResetAPIKeyMessageText": "Czy na pewno chcesz zresetować klucz API?", "RestartProwlarr": "Zrestartuj {appName}", @@ -402,7 +402,7 @@ "AppUpdatedVersion": "{appName} został zaktualizowany do wersji `{version}`, by uzyskać nowe zmiany należy przeładować {appName}", "AddCustomFilter": "Dodaj niestandardowy filtr", "AuthenticationMethodHelpTextWarning": "Wybierz prawidłową metodę uwierzytelniania", - "Any": "Dowolne", + "Any": "Dowolny", "AuthenticationMethod": "Metoda uwierzytelniania", "AuthenticationRequired": "Wymagane uwierzytelnianie", "Categories": "Kategorie", @@ -427,7 +427,7 @@ "InstallLatest": "Zainstaluj najnowsze", "Clone": "Klonuj", "Stats": "Status", - "CurrentlyInstalled": "Obecnie zainstalowane", + "CurrentlyInstalled": "Aktualnie zainstalowane", "Mixed": "Mieszane", "IndexerRss": "RSS indeksera", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Potwierdź nowe hasło", @@ -447,16 +447,16 @@ "MinimumSeeders": "Minimalni seederzy", "IndexerVipExpiredHealthCheckMessage": "Przywileje indeksera VIP się wyczerpały: {indexerNames}", "IndexerVipExpiringHealthCheckMessage": "Przywileje indeksera VIP wkrótce się wyczerpią: {indexerNames}", - "BlackholeFolderHelpText": "Folder, w którym {appName} zapisze plik {extension}", + "BlackholeFolderHelpText": "Folder, w którym {appName} będzie przechowywać plik {extension}", "DefaultCategory": "Domyślna kategoria", "DefaultNameCopiedProfile": "{name} - Kopia", "DownloadClientDelugeSettingsUrlBaseHelpText": "Dodaje prefiks do URL JSON Deluge, zobacz {url}", "Destination": "Miejsce docelowe", - "DownloadClientFreeboxSettingsAppTokenHelpText": "Token aplikacji uzyskany podczas tworzenia dostępu do API Freebox (tj. 'app_token')", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token aplikacji otrzymany podczas tworzenia dostępu do Freebox API ('app_token')", "DownloadClientPneumaticSettingsStrmFolder": "Folder Strm", "DownloadClientSettingsAddPaused": "Dodaj wstrzymane", "DownloadClientSettingsDestinationHelpText": "Ręcznie określa miejsce docelowe pobierania, pozostaw puste aby użyć domyślnego", - "DownloadClientSettingsInitialStateHelpText": "Stan początkowy torrentów dodawanych do {clientName}", + "DownloadClientSettingsInitialStateHelpText": "Stan początkowy torrentów dodanych do {clientName}", "DownloadClientSettingsPriorityItemHelpText": "Priorytet podczas zgarniania pozycji", "IndexerBeyondHDSettingsLimitedOnly": "Tylko limitowany", "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Szukaj tylko premier freeleech", @@ -474,7 +474,7 @@ "AppSettingsSummary": "Aplikacje i ustawienia do konfiguracji jak {appName} współpracuje z Twoimi programami PVR", "Author": "Autor", "ApplicationsLoadError": "Nie można załadować listy aplikacji", - "DownloadClientAriaSettingsDirectoryHelpText": "Opcjonalna lokalizacja pobrań, pozostaw puste aby użyć domyślnej lokalizacji Aria2", + "DownloadClientAriaSettingsDirectoryHelpText": "Opcjonalna lokalizacja dla pobrań, pozostaw puste jeżeli chcesz używać domyślnej lokalizacji Aria2", "EditSyncProfile": "Edytuj profil synchronizacji", "ManageClients": "Zarządzaj klientami", "PreferMagnetUrl": "Preferuj linki magnet", @@ -484,7 +484,7 @@ "QueryOptions": "Opcje wyszukiwania", "DownloadClientFloodSettingsUrlBaseHelpText": "Dodaje prefiks do API Flood, np. {url}", "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Pobieraj najpierw pierwsze i ostatnie fragmenty (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsUseSslHelpText": "Użyj bezpiecznego połączenia. Zobacz Opcje -> Web UI -> 'Use HTTPS instead of HTTP' w qBittorrent.", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Używaj bezpiecznego połączenia. Sprawdź Opcje -> Interfejs webowy -> 'Używaj HTTPS zamiast HTTP' w qBittorrent.", "DownloadClientRTorrentSettingsAddStopped": "Dodaj zatrzymane", "IndexerNzbIndexSettingsApiKeyHelpText": "Klucz API serwisu (opcjonalny — wyszukiwanie działa bez niego, ale z niższym limitem częstotliwości)", "Redirected": "Przekierowany", @@ -526,7 +526,7 @@ "DownloadClientFloodSettingsAdditionalTags": "Dodatkowe tagi", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Dodaje właściwości mediów jako tagi. Podpowiedzi są przykładami.", "DownloadClientFreeboxSettingsApiUrl": "URL API", - "DownloadClientFreeboxSettingsAppIdHelpText": "ID aplikacji podane podczas tworzenia dostępu do API Freebox (tj. 'app_id')", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID aplikacji przypisane podczas tworzenia dostępu do Freebox API ('app_id')", "DownloadClientFreeboxSettingsAppToken": "Token aplikacji", "DownloadClientPneumaticSettingsNzbFolderHelpText": "Ten folder musi być osiągalny z XBMC", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Pliki .strm w tym folderze będą importowane przez drone", @@ -575,7 +575,7 @@ "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Domyślna kategoria zapasowa jeżeli nie ma zmapowanej kategorii dla premiery. Dodawanie kategorii wyjątkowej dla {appName} pomaga uniknąć konfliktów z niezwiązanymi pobraniami spoza {appName}. Korzystanie z kategorii jest opcjonalne, ale mocno rekomendowane.", "AverageResponseTimesMs": "Średni czas reakcji indeksera (ms)", "Book": "Książka", - "EnableRssHelpText": "Włącz", + "EnableRssHelpText": "Włącz kanał RSS dla indeksatora", "FoundCountReleases": "Znaleziono {itemCount} premier", "AudioSearch": "Wyszukiwanie audio", "AreYouSureYouWantToDeleteCategory": "Czy na pewno chcesz usunąć zmapowaną kategorię?", @@ -595,7 +595,7 @@ "SyncLevel": "Poziom synchronizacji", "DeleteIndexerProxy": "Usuń proxy indeksera", "AdvancedSettingsHiddenClickToShow": "Zaawansowane ustawienia ukryte, kliknij aby pokazać", - "CountIndexersSelected": "Wybrano {count} indekser(y)", + "CountIndexersSelected": "Wybrane indeksery: {count}", "DeleteSelectedIndexer": "Usuń wybrany indekser", "FilterPlaceHolder": "Szukaj indekserów", "ManageDownloadClients": "Zarządzaj klientami pobierania", @@ -660,7 +660,7 @@ "DownloadClientFreeboxSettingsAppId": "ID aplikacji", "DownloadClientFreeboxSettingsPortHelpText": "Port używany do dostępu do interfejsu Freebox, domyślnie '{port}'", "DownloadClientNzbgetSettingsAddPausedHelpText": "Ta opcja wymaga co najmniej NzbGet w wersji 16.0", - "DownloadClientPneumaticSettingsNzbFolder": "Folder NZB", + "DownloadClientPneumaticSettingsNzbFolder": "Folder Nzb", "DownloadClientQbittorrentSettingsSequentialOrder": "Kolejność sekwencyjna", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Pobieraj w kolejności sekwencyjnej (qBittorrent 4.1.0+)", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Dodaje prefiks do URL RPC klienta {clientName}, np. {url}, domyślnie '{defaultUrl}'", @@ -694,7 +694,7 @@ "BookSearch": "Wyszukiwanie książek", "ClearHistoryMessageText": "Czy na pewno chcesz wyczyścić całą historię {appName}?", "DownloadClientFreeboxSettingsHostHelpText": "Nazwa hosta lub adres IP hosta Freebox, domyślnie '{url}' (zadziała tylko w tej samej sieci)", - "EditSelectedIndexers": "Edytuj zaznaczone indeksery", + "EditSelectedIndexers": "Edytuj wybrane indeksery", "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Szukaj tylko premier freeleech", "IndexerDetails": "Szczegóły indeksera", "IndexerGazelleGamesSettingsApiKeyHelpText": "Klucz API ze strony (do znalezienia w Ustawienia => Ustawienia Dostępu)", @@ -705,13 +705,13 @@ "ProwlarrSupportsAnyDownloadClient": "{appName} wspiera każdy z poniższych klientów pobierania.", "AddCategory": "Dodaj kategorię", "ClickToChangeQueryOptions": "Kliknij aby zmienić opcje wyszukiwania", - "CountDownloadClientsSelected": "Wybrano {count} klient(ów) pobierania", + "CountDownloadClientsSelected": "Wybrani klienci pobierania: {count}", "CountIndexersAvailable": "Dostępne indeksery: {count}", "Description": "Opis", - "Donate": "Wesprzyj", + "Donate": "Wesprzyj finansowo", "DownloadClientCategory": "Kategoria klienta pobierania", "EditCategory": "Edytuj kategorię", - "EditSelectedDownloadClients": "Edytuj zaznaczonych klientów pobierania", + "EditSelectedDownloadClients": "Edytuj wybranych klientów pobierania", "DownloadClientTransmissionSettingsDirectoryHelpText": "Opcjonalna lokalizacja pobrań, pozostaw puste aby użyć domyślnej lokalizacji Transmission", "IndexerBeyondHDSettingsRefundOnly": "Tylko zwroty", "IndexerBeyondHDSettingsRssKeyHelpText": "Klucz RSS ze strony (do znalezienia w Moje Bezpieczeństwo => Klucz RSS)", @@ -763,7 +763,7 @@ "AppProfileSelectHelpText": "Profile aplikacji są wykorzystywane do kontroli RSS oraz ustawień automatycznego i interaktywnego wyszukiwania podczas synchronizacji", "IndexerMTeamTpSettingsApiKeyHelpText": "Klucz API ze strony (do znalezienia w Panel kontrolny użytkownika => Laboratorium)", "IndexerNebulanceSettingsApiKeyHelpText": "Klucz API z Ustawień użytkownika => Kluczy API. Klucz musi mieć pozwolenia listowania i pobierania", - "DownloadClientRTorrentSettingsAddStoppedHelpText": "Włączenie spowoduje dodawanie torrentów i magnetów do rTorrent w stanie zatrzymanym. To może psuć pliki magnet.", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Włączenie doda torrenty i magnety do rTorrenta w zatrzymanym stanie. To może uszkodzić pliki magnet.", "DownloadClientRTorrentSettingsUrlPathHelpText": "Ścieżka do endpointu XMLRPC, zobacz {url}. Zwykle jest to RPC2 lub [ścieżka do ruTorrent]{url2} przy użyciu ruTorrent.", "IndexerAuth": "Autoryzacja indeksera", "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Jeżeli torrent jest zablokowany przez hasha może zostać nieprawidłowo odrzucony podczas RSS/Szukania przez niektóre indeksery, włączenie tej opcji pozwoli na jego odrzucenie po zgarnięciu, ale przed wysłaniem do klienta.", @@ -807,5 +807,7 @@ "DownloadClientUTorrentProviderMessage": "uTorrent ma historię dołączania cryptominerów, malware i reklam, zdecydowanie zachęcamy do wybrania innego klienta.", "ThemeHelpText": "Zmień motyw interfejsu użytkownika aplikacji, 'Auto' motyw będzie używał motywu Twojego systemu operacyjnego aby ustawić tryb Jasny lub Ciemny. Zainspirowane przez {inspiredBy}.", "DownloadClientSettingsDefaultCategoryHelpText": "Domyślna kategoria zapasowa jeżeli nie ma zmapowanej kategorii dla premiery. Dodawanie kategorii wyjątkowej dla {appName} pomaga uniknąć konfliktów z niezwiązanymi pobraniami spoza {appName}. Korzystanie z kategorii jest opcjonalne, ale mocno rekomendowane.", - "HealthMessagesInfoBox": "Więcej informacji o przyczynie tych komunikatów kontroli stanu znajdziesz, klikając link wiki (ikona książki) na końcu wiersza lub sprawdzając [logi]({link}). Jeśli masz trudność z interpretacją tych komunikatów, możesz skontaktować się z naszym wsparciem przez linki poniżej." + "HealthMessagesInfoBox": "Więcej informacji o przyczynie tych komunikatów kontroli stanu znajdziesz, klikając link wiki (ikona książki) na końcu wiersza lub sprawdzając [logi]({link}). Jeśli masz trudność z interpretacją tych komunikatów, możesz skontaktować się z naszym wsparciem przez linki poniżej.", + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Wybierz sposób sortowania wyników wyszukiwania", + "IndexerIPTorrentsSettingsSearchSortBy": "Sortuj według" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 3773b9adefc..877af3b9a75 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -141,7 +141,7 @@ "EnableRss": "Habilitar RSS", "EnableRssHelpText": "Habilitar feed RSS para o indexador", "EnableSSL": "Habilitar SSL", - "EnableSslHelpText": " Requer a reinicialização com a execução como administrador para fazer efeito", + "EnableSslHelpText": " Requer a reinicialização como administrador para entrar em vigor", "Enabled": "Habilitado", "EnabledRedirected": "Habilitado, Redirecionado", "Encoding": "Codificação", @@ -813,5 +813,10 @@ "ApplicationAuthUsername": "Usuário de autenticação", "ApplicationAuthUsernameHelpText": "Nome de usuário de autenticação básica HTTP usado pelo {appName} ao acessar este aplicativo. Defina se o aplicativo estiver atrás de um proxy reverso protegido por autenticação básica.", "IndexerIPTorrentsSettingsSearchSortBy": "Ordenar Busca Por", - "IndexerIPTorrentsSettingsSearchSortByHelpText": "Selecione a ordem na qual classificar os resultados da busca" + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Selecione a ordem na qual classificar os resultados da busca", + "AllowedHosts": "Hosts permitidos", + "AllowedHostsHelpText": "Lista separada por vírgulas de nomes de host e endereços IP para os quais o {appName} aceitará solicitações. Use '*.' como um curinga para subdomínios. Deixe em branco para permitir todos os hosts.", + "AllowedHostsNotConfiguredHealthCheckMessage": "Hosts permitidos não estão configurados, o {appName} aceitará solicitações de qualquer nome de host. Defina Hosts permitidos em Configurações gerais para restringir quais nomes de host podem ser usados para acessar o {appName}.", + "TrustedNetworks": "Redes Confiáveis", + "TrustedNetworksHelpText": "Lista separada por vírgulas de endereços IP ou redes em notação CIDR em que os proxies reversos confiáveis estão ativados. por exemplo 172.17.0.1, 10.0.0.0/8 ou fc00::/7. Adicione apenas proxies configurados corretamente para enviar os cabeçalhos corretos." } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index bc215c40708..32a5ac2cc8c 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -30,7 +30,7 @@ "GeneralSettingsSummary": "Port, SSL, utilizator/parolă, proxy, statistici și actualizări", "General": "General", "Folder": "Dosar", - "Filter": "Filtru", + "Filter": "Filtrare", "Files": "Fișiere", "Filename": "Nume fișier", "Failed": "Eșuat", @@ -526,5 +526,7 @@ "MovieSearchTypes": "Caută filme în masa", "PrioritySettings": "Prioritate: {0}", "TheLogLevelDefault": "Nivelul jurnalului este implicit la „Informații” și poate fi modificat în", - "WouldYouLikeToRestoreBackup": "Doriți să restaurați copia de rezervă {0}?" + "WouldYouLikeToRestoreBackup": "Doriți să restaurați copia de rezervă {0}?", + "ApplicationAuthPassword": "Parolă autentificare", + "ApplicationAuthUsername": "Nume utilizator autentificare" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index e509c143d65..dc14cf8518e 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -5,14 +5,14 @@ "Backups": "Резервные копии", "BackupRetentionHelpText": "Автоматические резервные копии старше указанного периода будут автоматически удалены", "BackupNow": "Создать резервную копию", - "BackupIntervalHelpText": "Периодичность автоматического резервного копирования", + "BackupIntervalHelpText": "Интервал между автоматическим резервным копированием", "Backup": "Резервное копирование", "AutomaticSearch": "Автоматический поиск", - "Authentication": "Авторизация", + "Authentication": "Аутентификация", "ApplyTags": "Применить теги", "Apply": "Применить", - "AppDataLocationHealthCheckMessage": "Обновление не позволит сохранить AppData при обновлении", - "ApiKey": "API ключ", + "AppDataLocationHealthCheckMessage": "Обновление не сможет избежать удаления AppData", + "ApiKey": "API-ключ", "Analytics": "Аналитика", "AddIndexer": "Добавить индексатор", "Added": "Добавлено", @@ -87,16 +87,16 @@ "Level": "Уровень", "Ok": "Ок", "AddDownloadClient": "Добавить клиент загрузки", - "UpdateMechanismHelpText": "Использовать встроенный инструмент обновления {appName} или скрипт", + "UpdateMechanismHelpText": "Использовать встроенный в {appName} инструмент обновления или скрипт", "IndexerStatusUnavailableHealthCheckMessage": "Индексаторы недоступны из-за ошибок: {indexerNames}", "NoTagsHaveBeenAddedYet": "Теги ещё не добавлены", "UnableToLoadTags": "Не удалось загрузить теги", - "AnalyticsEnabledHelpText": "Отправлять анонимную информацию об использовании и ошибках на серверы {appName}. Анонимная статистика включает в себя информацию о вашем браузере, какие страницы веб-интерфейса {appName} вы используете, отчёты об ошибках, а также версию операционной системы и среды выполнения. Мы будем использовать эту информацию для разработки нового функционала и исправления ошибок.", + "AnalyticsEnabledHelpText": "Отправлять анонимную информацию об использовании и ошибках на серверы {appName}. Анонимная статистика включает в себя информацию о вашем браузере, какие страницы веб-интерфейса {appName} вы используете, отчёты об ошибках, а также версию ОС и среды выполнения. Мы будем использовать эту информацию для разработки нового функционала и исправления ошибок.", "AuthenticationMethodHelpText": "Необходимо ввести имя пользователя и пароль для доступа к {appName}", - "BackupFolderHelpText": "Относительные пути будут в каталоге AppData {appName}", + "BackupFolderHelpText": "Относительные пути будут в подкаталоге AppData {appName}", "BeforeUpdate": "До обновления", "BindAddress": "Привязать адрес", - "BindAddressHelpText": "Действительный IP-адрес, локальный адрес или '*' для всех интерфейсов", + "BindAddressHelpText": "Действительный IP-адрес, localhost или '*' для всех интерфейсов", "Branch": "Ветвь", "BranchUpdate": "Ветвь для обновления {appName}", "BranchUpdateMechanism": "Ветвь, используемая внешним механизмом обновления", @@ -306,7 +306,7 @@ "Tags": "Теги", "ApplicationStatusCheckSingleClientMessage": "Приложения недоступны из-за ошибок: {0}", "EditIndexer": "Редактировать индексатор", - "MaintenanceRelease": "Технический релиз: исправление ошибок и другие улучшения. Подробнее см. в истории коммитов Github.", + "MaintenanceRelease": "Технический релиз: исправление ошибок и другие улучшения. Подробнее см. в истории коммитов Github", "Filters": "Фильтры", "HistoryCleanupDaysHelpText": "Установите 0, чтобы отключить автоматическую очистку", "HistoryCleanupDaysHelpTextWarning": "Элементы истории, старше указанного количества дней, будут автоматически удалены", @@ -363,7 +363,7 @@ "DeleteSelectedIndexersMessageText": "Вы уверены, что хотите удалить выбранные индексаторы: {count}?", "DownloadClientPriorityHelpText": "Установить приоритет для нескольких клиентов загрузки. Клиенты с одинаковым приоритетом обрабатываются по круговой системе.", "EditSelectedDownloadClients": "Редактировать выбранные клиенты загрузки", - "ApiKeyValidationHealthCheckMessage": "Пожалуйста, обновите свой ключ API, чтобы он был длиной не менее {length} символов в длину. Вы можете сделать это через настройки или файл конфигурации", + "ApiKeyValidationHealthCheckMessage": "Пожалуйста, измените свой ключ API, так чтобы он был длиной не менее {length} символов. Вы можете сделать это через настройки или файл конфигурации", "Genre": "Жанр", "Theme": "Тема", "Year": "Год", @@ -415,12 +415,12 @@ "ResetAPIKeyMessageText": "Вы уверены, что хотите сбросить ключ API?", "Categories": "Категории", "Album": "Альбом", - "AddCustomFilter": "Добавить специальный фильтр", + "AddCustomFilter": "Добавить пользовательский фильтр", "AuthenticationMethod": "Способ авторизации", "AuthenticationRequiredPasswordHelpTextWarning": "Введите новый пароль", "AuthenticationRequiredUsernameHelpTextWarning": "Введите новое имя пользователя", "RestartProwlarr": "Перезапустить {appName}", - "AuthenticationRequiredWarning": "Чтобы предотвратить удалённый доступ без авторизации, {appName} теперь требует включения авторизации. Вы можете опционально отключить авторизацию для локальных адресов.", + "AuthenticationRequiredWarning": "Чтобы предотвратить удалённый доступ без авторизации, {appName} требует её включения. Вы можете отключить авторизацию для локальных адресов.", "Id": "ID", "ManageClients": "Управление клиентами", "CountApplicationsSelected": "Выбрано приложений: {count}", @@ -437,7 +437,7 @@ "ProxyValidationBadRequest": "Не удалось протестировать прокси. Код состояния: {statusCode}", "Default": "По умолчанию", "AppUpdated": "{appName} обновлён", - "AppUpdatedVersion": "{appName} обновлён до версии `{version}`, для получения последних изменений необходимо перезагрузить {appName}", + "AppUpdatedVersion": "{appName} обновлено до версии `{version}`, для получения последних изменений необходимо перезагрузить {appName}", "Episode": "Эпизод", "Donate": "Пожертвовать", "DownloadClientFreeboxSettingsAppTokenHelpText": "Токен приложения, полученный при создании доступа к Freebox API (например, 'app_token')", @@ -502,11 +502,11 @@ "TorrentBlackholeTorrentFolder": "Каталог торрента", "TorrentBlackholeSaveMagnetFilesHelpText": "Сохранить магнет-ссылку, если файл .torrent недоступен (полезно только в случае, если клиент загрузки поддерживает магнет-ссылки, сохранённые в файле)", "NotificationsEmailSettingsUseEncryption": "Использовать шифрование", - "NotificationsEmailSettingsUseEncryptionHelpText": "Выбрать режим шифрования: предпочитать шифрование, если оно настроено на сервере; всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт); никогда не использовать шифрование.", + "NotificationsEmailSettingsUseEncryptionHelpText": "Выбрать режим шифрования: предпочитать шифрование, если оно настроено на сервере; всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт); никогда не использовать шифрование", "LabelIsRequired": "Требуется метка", "IndexerSettingsSeedTime": "Время сидирования", "Mixed": "Смешанный", - "IndexerSettingsApiPath": "Путь API", + "IndexerSettingsApiPath": "Путь к API", "IndexerSettingsSeedRatioHelpText": "Рейтинг, которого должен достичь торрент перед остановкой, пусто — используется значение по умолчанию клиента загрузки. Рейтинг должен быть не менее 1,0 и соответствовать правилам индексаторов", "InvalidUILanguage": "В вашем пользовательском интерфейсе установлен недопустимый язык. Исправьте его и сохраните настройки", "IndexerHDBitsSettingsCodecsHelpText": "Если не указано, используются все параметры.", @@ -617,7 +617,7 @@ "IndexerSettingsQueryLimit": "Лимит запросов", "IndexerSettingsRssKey": "Ключ RSS", "Parameters": "Параметры", - "RedirectHelpText": "Перенаправить входящий запрос на загрузку для индексатора и передать захват напрямую, вместо проксирования запроса через {appName}.", + "RedirectHelpText": "Перенаправить входящий запрос на загрузку для индексатора и передать захват напрямую, вместо проксирования запроса через {appName}", "SettingsFilterSentryEvents": "Фильтровать события аналитики", "NotSupported": "Не поддерживается", "SyncLevel": "Уровень синхронизации", @@ -643,7 +643,7 @@ "DisabledUntil": "Отключено до", "SearchType": "Тип поиска", "AppSettingsSummary": "Приложения и настройки для управления взаимодействием {appName} с вашими программами PVR", - "SettingsFilterSentryEventsHelpText": "Фильтровать известные события ошибок пользователя, чтобы они не отправлялись в виде аналитики.", + "SettingsFilterSentryEventsHelpText": "Фильтровать известные события ошибок пользователя, чтобы они не отправлялись в виде аналитики", "MovieSearchTypes": "Типы поиска фильма", "Apps": "Программы", "Auth": "Авторизация", @@ -679,7 +679,7 @@ "DefaultCategory": "Категория по умолчанию", "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Показать только релизы с freeleech", "IndexerHDBitsSettingsOrigins": "Источники", - "IndexerNzbIndexSettingsApiKeyHelpText": "Ключ API сайта", + "IndexerNzbIndexSettingsApiKeyHelpText": "Ключ API сайта (необязательно — поиск работает и без него, но с меньшим лимитом запросов)", "IndexerOrpheusSettingsApiKeyHelpText": "API ключ сайта (Находится в Настройки => Настройки доступа)", "IndexerSettingsApiUser": "Пользователь API", "IndexerSettingsAppsMinimumSeeders": "Приложения Минимальное количество сидеров", @@ -695,7 +695,7 @@ "IndexerHDBitsSettingsUsernameHelpText": "Имя пользователя сайта", "IndexerSettingsCookieHelpText": "Cookie сайта", "IndexerSettingsFreeleechOnly": "Только с freeleech", - "IndexerSettingsPasskey": "Pass Key", + "IndexerSettingsPasskey": "Парольный ключ", "IndexerId": "ID индексатора", "DeleteSelectedIndexers": "Удалить выбранные индексаторы", "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Требуются разрешения 'Пользователь' и 'Торренты'", @@ -707,7 +707,7 @@ "IndexerNoDefinitionCheckHealthCheckMessage": "Индексаторы {indexerNames} не имеют определения и не работают. Удалите их из {appName} и добавьте снова.", "AuthQueries": "Запросы авторизации", "IndexerNewznabSettingsVipExpirationHelpText": "Введите дату (yyyy-mm-dd) окончания VIP или оставьте поле пустым, {appName} оповестит вас за неделю до окончания VIP", - "IndexerPassThePopcornSettingsApiUserHelpText": "Эти настройки расположены в разделе безопасности PassThePopcorn (Редактировать профиль > Безопасность).", + "IndexerPassThePopcornSettingsApiUserHelpText": "Эти настройки находятся в разделе безопасности PassThePopcorn (Редактировать профиль > Безопасность).", "IndexerSettingsBaseUrlHelpText": "Выберите основной URL-адрес, который {appName} будет использовать для запросов на сайт", "Open": "Открыть", "IndexerInfo": "Информация об индексаторе", @@ -738,7 +738,7 @@ "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Категория по умолчанию, если для релиза нет соответствующей категории. Создание определённой категории для {appName}, позволяет избежать конфликтов с загрузками, не связанными с {appName}. Использование категории не обязательно, но настоятельно рекомендуется. Создаёт подкаталог [category] в каталоге вывода.", "AverageQueries": "Среднее количество запросов", "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", - "IndexerMTeamTpSettingsApiKeyHelpText": "Ключ API сайта (Находится в Панели управления пользователя => Безопасность => Лаборатория)", + "IndexerMTeamTpSettingsApiKeyHelpText": "Ключ API с сайта (Находится в Панели управления пользователя => Лаборатория)", "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", "ProwlarrDownloadClientsAlert": "Если вы намерены выполнять поиск непосредственно в {appName}, вам необходимо добавить клиенты загрузки. В противном случае, добавлять их здесь не нужно. Для поиска из ваших приложений используются клиенты загрузки, которые настроены в самих приложениях.", "ProwlarrDownloadClientsInAppOnlyAlert": "Клиенты загрузки предназначены только для поиска внутри приложения {appName} и не синхронизируются с другими приложениями. Мы не планируем добавлять функцию синхронизации.", @@ -751,7 +751,7 @@ "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Искать только с freeleech (Лимитированная отдача)", "IndexerBeyondHDSettingsRefundOnly": "Только возврат", "IndexerBeyondHDSettingsRssKeyHelpText": "Ключ RSS с сайта (Находится в Моя безопасность => Ключ RSS)", - "IndexerDownloadClientHelpText": "Определите клиент загрузки, используемый для загрузки из этого индексатора в {appName}.", + "IndexerDownloadClientHelpText": "Определите клиент загрузки, используемый для загрузки из этого индексатора в {appName}", "NewznabUrl": "URL-адрес Newznab", "SeedTimeHelpText": "Время, в течение которого торрент должен оставаться на раздаче перед остановкой, пусто — используется значение приложения по умолчанию", "IndexerFileListSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", @@ -807,5 +807,16 @@ "FailedToFetchSettings": "Не удалось загрузить настройки", "CurrentlyInstalled": "Установлено", "PreviouslyInstalled": "Ранее установленный", - "DownloadClientUTorrentProviderMessage": "Мы настоятельно советуем не использовать uTorrent, т.к. он известен как программа-шифровальщик и в целом вредоносное ПО." + "DownloadClientUTorrentProviderMessage": "Мы настоятельно советуем не использовать uTorrent, т.к. он известен как криптомайнер и в целом вредоносное ПО. Настоятельно рекомендуем выбрать другой клиент.", + "ApplicationAuthPassword": "Пароль Auth", + "ApplicationAuthPasswordHelpText": "Пароль HTTP Basic Auth, который использует {appName} для доступа к приложению. Установите, если приложение работает за reverse‑proxy с Basic‑аутентификацией.", + "ApplicationAuthUsername": "Имя пользователя Auth", + "ApplicationAuthUsernameHelpText": "Имя пользователя HTTP Basic Auth, которое использует {appName} для доступа к приложению. Установите, если приложение работает за reverse‑proxy с Basic‑аутентификацией.", + "IndexerIPTorrentsSettingsSearchSortBy": "Cортировка поиска по", + "IndexerIPTorrentsSettingsSearchSortByHelpText": "Выберите порядок сортировки результатов поиска", + "AllowedHosts": "Разрешенные хосты", + "AllowedHostsHelpText": "Список имен и IP-адресов, разделенных запятыми, с которых {appName} будет принимать запросы. Для поддоменов используйте символ подстановки '*.' . Оставьте пустым, чтобы разрешить доступ отовсюду.", + "AllowedHostsNotConfiguredHealthCheckMessage": "Параметр «Разрешенные хосты» не настроен, поэтому {appName} будет принимать запросы отовсюду. Укажите «Разрешенные хосты» в основныъ настройках, чтобы ограничить список имён хостов, с которых можно обращаться к {appName}.", + "TrustedNetworks": "Доверенные сети", + "TrustedNetworksHelpText": "Разделенный запятыми список IP-адресов или сетей в нотации CIDR, в которых находятся доверенные обратные прокси-серверы. Например: 172.17.0.1, 10.0.0.0/8 или fc00::/7. Добавляйте только те прокси-серверы, которые правильно настроены для отправки корректных заголовков." } diff --git a/src/NzbDrone.Core/Localization/Core/sv.json b/src/NzbDrone.Core/Localization/Core/sv.json index e8505a53d43..7975d42f8b0 100644 --- a/src/NzbDrone.Core/Localization/Core/sv.json +++ b/src/NzbDrone.Core/Localization/Core/sv.json @@ -476,14 +476,14 @@ "Proxies": "Profiler", "Donate": "Datum", "TvSearch": "Sök", - "AddCategory": "Kategori", - "AddConnection": "Anslutning", + "AddCategory": "Lägg till kategori", + "AddConnection": "Lägg till anslutning", "EditSelectedDownloadClients": "Radera nedladdningsklient", "MovieSearch": "Mass-sök filmer", "IndexerHDBitsSettingsOrigins": "Orginal", "IndexerInfo": "Indexerare", "MovieSearchTypes": "Mass-sök filmer", - "AddSyncProfile": "Redigera fördröjningsprofil", + "AddSyncProfile": "Lägg till synkroniseringsprofil", "SyncProfile": "Redigera fördröjningsprofil", "SyncProfiles": "Redigera fördröjningsprofil", "DownloadClientCategory": "Radera nedladdningsklient", @@ -495,5 +495,6 @@ "EditCategory": "Kategori", "PrioritySettings": "Prioritet: {0}", "TheLogLevelDefault": "Loggnivån är som standard 'Info' och kan ändras i", - "WouldYouLikeToRestoreBackup": "Skulle du vilja återställa säkerhetskopian {0} ?" + "WouldYouLikeToRestoreBackup": "Skulle du vilja återställa säkerhetskopian {0} ?", + "ActiveApps": "Aktiva appar" } diff --git a/src/NzbDrone.Core/Localization/Core/vi.json b/src/NzbDrone.Core/Localization/Core/vi.json index 184b36a9162..36f4e505ec9 100644 --- a/src/NzbDrone.Core/Localization/Core/vi.json +++ b/src/NzbDrone.Core/Localization/Core/vi.json @@ -82,7 +82,7 @@ "Backups": "Sao lưu", "BeforeUpdate": "Trước khi cập nhật", "CertificateValidationHelpText": "Thay đổi cách xác thực chứng chỉ HTTPS nghiêm ngặt", - "About": "Trong khoảng", + "About": "Về chúng tôi", "AcceptConfirmationModal": "Phương thức xác nhận chấp nhận", "Actions": "Hành động", "Added": "Thêm", @@ -446,5 +446,6 @@ "ApplicationURL": "URL của ứng dụng", "PrioritySettings": "Ưu tiên: {0}", "TheLogLevelDefault": "Mức nhật ký mặc định là 'Thông tin' và có thể được thay đổi trong", - "WouldYouLikeToRestoreBackup": "Bạn có muốn khôi phục bản sao lưu {0} không?" + "WouldYouLikeToRestoreBackup": "Bạn có muốn khôi phục bản sao lưu {0} không?", + "Any": "bất kỳ" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 7ec240f48b7..83dcf3036f8 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1,7 +1,7 @@ { "About": "关于", "AcceptConfirmationModal": "接受确认对话框", - "Actions": "动作", + "Actions": "操作", "Add": "添加", "AddApplication": "添加应用程序", "AddCustomFilter": "添加自定义过滤器", @@ -24,7 +24,7 @@ "ApiKey": "API 密钥", "ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少 {length} 个字符长。您可以通过设置或配置文件完成此操作", "AppDataDirectory": "AppData 目录", - "AppDataLocationHealthCheckMessage": "为防止在更新时删除 AppData,更新将无法进行", + "AppDataLocationHealthCheckMessage": "为防止在更新时删除应用数据,更新将无法进行", "AppProfileInUse": "正在使用的应用程序配置文件", "AppProfileSelectHelpText": "应用程序配置用于控制应用程序同步设置 RSS、自动搜索和交互式搜索设置", "AppSettingsSummary": "配置{appName}与PVR程序交互方式的应用和设置", @@ -806,5 +806,7 @@ "ProwlarrDownloadClientsInAppOnlyAlert": "下载管理器仅对 {appName} 中搜索有效,而不会同步到其他程序中。目前没有添加此功能的计划。", "IndexerNewznabSettingsAdditionalParametersHelpText": "額外的 Newznab 參數(例如 `&t=search&cat=2000,2010&extended=1`)", "IndexerSettingsApiUser": "API 使用者", - "ApplicationAuthPassword": "密码" + "ApplicationAuthPassword": "密码", + "TrustedNetworks": "可信网络", + "AllowedHosts": "允许的主机" } diff --git a/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs b/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs index 5ddf12fc07b..6672d9962bb 100644 --- a/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs +++ b/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs @@ -5,7 +5,9 @@ using NLog; using NzbDrone.Common; using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Messaging; +using NzbDrone.Common.Reflection; using NzbDrone.Common.TPL; namespace NzbDrone.Core.Messaging.Events @@ -14,6 +16,7 @@ public class EventAggregator : IEventAggregator { private readonly Logger _logger; private readonly IServiceFactory _serviceFactory; + private readonly IRuntimeInfo _runtimeInfo; private readonly TaskFactory _taskFactory; private readonly Dictionary _eventSubscribers; @@ -38,10 +41,11 @@ public EventSubscribers(IServiceFactory serviceFactory) } } - public EventAggregator(Logger logger, IServiceFactory serviceFactory) + public EventAggregator(Logger logger, IServiceFactory serviceFactory, IRuntimeInfo runtimeInfo) { _logger = logger; _serviceFactory = serviceFactory; + _runtimeInfo = runtimeInfo; _taskFactory = new TaskFactory(); _eventSubscribers = new Dictionary(); } @@ -53,6 +57,12 @@ public void PublishEvent(TEvent @event) var eventName = GetEventName(@event.GetType()); + if (_runtimeInfo.IsExiting && @event.GetType().HasAttribute()) + { + _logger.Warn("Event {0} blocked due to application shutdown", eventName); + return; + } + /* int workerThreads; int completionPortThreads; @@ -77,7 +87,15 @@ public void PublishEvent(TEvent @event) { if (!_eventSubscribers.TryGetValue(eventName, out var target)) { - _eventSubscribers[eventName] = target = new EventSubscribers(_serviceFactory); + try + { + _eventSubscribers[eventName] = target = new EventSubscribers(_serviceFactory); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to resolve event subscribers for {0}, container may be disposed", eventName); + return; + } } subscribers = target as EventSubscribers; diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 6ba6f6bf500..7a23d3b9b89 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -33,7 +33,7 @@ public override void OnGrab(GrabMessage message) }, Title = RELEASE_GRABBED_TITLE, Description = message.Message, - Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + Timestamp = DateTime.UtcNow.ToString("O"), Color = message.Successful ? (int)DiscordColors.Success : (int)DiscordColors.Danger, Fields = new List() }; @@ -92,7 +92,7 @@ public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) }, Title = healthCheck.Source.Name, Description = healthCheck.Message, - Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + Timestamp = DateTime.UtcNow.ToString("O"), Color = healthCheck.Type == HealthCheck.HealthCheckResult.Warning ? (int)DiscordColors.Warning : (int)DiscordColors.Danger }; @@ -112,7 +112,7 @@ public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) }, Title = "Health Issue Resolved: " + previousCheck.Source.Name, Description = $"The following issue is now resolved: {previousCheck.Message}", - Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + Timestamp = DateTime.UtcNow.ToString("O"), Color = (int)DiscordColors.Success }; @@ -131,7 +131,7 @@ public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) IconUrl = "https://raw.githubusercontent.com/Prowlarr/Prowlarr/develop/Logo/256.png" }, Title = APPLICATION_UPDATE_TITLE, - Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + Timestamp = DateTime.UtcNow.ToString("O"), Color = (int)DiscordColors.Standard, Fields = new List { diff --git a/src/NzbDrone.Core/Prowlarr.Core.csproj b/src/NzbDrone.Core/Prowlarr.Core.csproj index 4dba47ce05e..0316d4636b9 100644 --- a/src/NzbDrone.Core/Prowlarr.Core.csproj +++ b/src/NzbDrone.Core/Prowlarr.Core.csproj @@ -9,7 +9,6 @@ - @@ -17,13 +16,11 @@ - - - + diff --git a/src/NzbDrone.Core/Validation/IpValidation.cs b/src/NzbDrone.Core/Validation/IpValidation.cs index f4afa1f66be..81d6b10d9c9 100644 --- a/src/NzbDrone.Core/Validation/IpValidation.cs +++ b/src/NzbDrone.Core/Validation/IpValidation.cs @@ -1,5 +1,6 @@ using FluentValidation; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Network; namespace NzbDrone.Core.Validation { @@ -9,5 +10,10 @@ public static IRuleBuilderOptions ValidIpAddress(this IRuleBuilder { return ruleBuilder.Must(x => x.IsValidIpAddress()).WithMessage("Must contain wildcard (*) or a valid IP Address"); } + + public static IRuleBuilderOptions ValidIpNetworks(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must(IPNetworkParser.IsValidList).WithMessage("Must be a comma separated list of IP addresses or networks in CIDR notation, such as 172.17.0.1, 10.0.0.0/8 or fc00::/7"); + } } } diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index d58979e7a68..15972ac5f98 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -1,8 +1,10 @@ using System; +using System.Linq; using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; namespace NzbDrone.Core.Validation { @@ -32,6 +34,17 @@ public static IRuleBuilderOptions ValidHost(this IRuleBuilder HostRegex.IsMatch(x) || x.IsValidIpAddress()).WithMessage("must be valid Host without http://"); } + public static IRuleBuilderOptions ValidHosts(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must(x => + { + var hosts = AllowedHostsParser.Parse(x); + + return hosts.Any() && hosts.All(AllowedHostsParser.IsValidHost); + }) + .WithMessage("Must be a comma separated list of host names or IP addresses only, without a protocol or port"); + } + public static IRuleBuilderOptions ValidRootUrl(this IRuleBuilder ruleBuilder) { ruleBuilder.SetValidator(new NotEmptyValidator(null)); diff --git a/src/NzbDrone.Host.Test/ForwardedHeadersConfiguratorFixture.cs b/src/NzbDrone.Host.Test/ForwardedHeadersConfiguratorFixture.cs new file mode 100644 index 00000000000..b15bbe9ef27 --- /dev/null +++ b/src/NzbDrone.Host.Test/ForwardedHeadersConfiguratorFixture.cs @@ -0,0 +1,72 @@ +using System.Linq; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Configuration; +using NzbDrone.Host; + +namespace NzbDrone.App.Test +{ + [TestFixture] + public class ForwardedHeadersConfiguratorFixture + { + private static ForwardedHeadersOptions GetOptions(string trustedNetworks) + { + var configFileProvider = new Mock(); + configFileProvider.SetupGet(c => c.TrustedNetworks).Returns(trustedNetworks); + + var options = new ForwardedHeadersOptions(); + + ForwardedHeadersConfigurator.Configure(options, configFileProvider.Object); + + return options; + } + + private static string[] GetKnownNetworks(ForwardedHeadersOptions options) + { + return options.KnownNetworks.Select(n => $"{n.Prefix}/{n.PrefixLength}").ToArray(); + } + + [Test] + public void should_forward_for_proto_and_host_headers() + { + GetOptions("10.0.0.0/8").ForwardedHeaders + .Should() + .Be(ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void should_only_trust_loopback_when_no_networks_are_configured(string trustedNetworks) + { + GetKnownNetworks(GetOptions(trustedNetworks)).Should().BeEquivalentTo("127.0.0.1/8"); + } + + [Test] + public void should_trust_configured_networks_in_addition_to_loopback() + { + GetKnownNetworks(GetOptions("10.0.0.0/8, 172.17.0.1,fc00::/7")) + .Should() + .BeEquivalentTo("127.0.0.1/8", "10.0.0.0/8", "172.17.0.1/32", "fc00::/7"); + } + + [Test] + public void should_skip_invalid_networks() + { + GetKnownNetworks(GetOptions("10.0.0.0/8,not-an-address,10.1.2.3/8,0.0.0.0/0,192.168.0.0/16")) + .Should() + .BeEquivalentTo("127.0.0.1/8", "10.0.0.0/8", "192.168.0.0/16"); + } + + [Test] + public void should_skip_empty_entries() + { + GetKnownNetworks(GetOptions("10.0.0.0/8,, ,192.168.0.0/16,")) + .Should() + .BeEquivalentTo("127.0.0.1/8", "10.0.0.0/8", "192.168.0.0/16"); + } + } +} diff --git a/src/NzbDrone.Host/AppLifetime.cs b/src/NzbDrone.Host/AppLifetime.cs index 45616e36b21..bad3df94534 100644 --- a/src/NzbDrone.Host/AppLifetime.cs +++ b/src/NzbDrone.Host/AppLifetime.cs @@ -18,7 +18,6 @@ public class AppLifetime : IHostedService, IHandle private readonly IRuntimeInfo _runtimeInfo; private readonly IStartupContext _startupContext; private readonly IBrowserService _browserService; - private readonly IProcessProvider _processProvider; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -36,7 +35,6 @@ public AppLifetime(IHostApplicationLifetime appLifetime, _runtimeInfo = runtimeInfo; _startupContext = startupContext; _browserService = browserService; - _processProvider = processProvider; _eventAggregator = eventAggregator; _logger = logger; @@ -70,12 +68,13 @@ private void OnAppStarted() private void OnAppStopped() { - if (_runtimeInfo.RestartPending && !_runtimeInfo.IsWindowsService) + if (_runtimeInfo.RestartPending) { - var restartArgs = GetRestartArgs(); - - _logger.Info("Attempting restart with arguments: {0}", restartArgs); - _processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, restartArgs); + _logger.Info("Restart pending."); + } + else + { + _logger.Info("Application stopped without restart pending"); } } @@ -87,33 +86,21 @@ private void Shutdown() _appLifetime.StopApplication(); } - private string GetRestartArgs() - { - var args = _startupContext.PreservedArguments; - - args += " /restart"; - - if (!args.Contains("/nobrowser")) - { - args += " /nobrowser"; - } - - return args; - } - [EventHandleOrder(EventHandleOrder.Last)] public void Handle(ApplicationShutdownRequested message) { - if (!_runtimeInfo.IsWindowsService) + if (message.Restarting) { - if (message.Restarting) - { - _runtimeInfo.RestartPending = true; - } - + _runtimeInfo.RestartPending = true; + _logger.Debug("Restart requested"); + } + else + { + _logger.Debug("Shutdown requested"); LogManager.Configuration = null; - Shutdown(); } + + Shutdown(); } } } diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 589890bb25f..ca268a4aec0 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -6,6 +6,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading; using DryIoc; using DryIoc.Microsoft.DependencyInjection; using Microsoft.AspNetCore.Hosting; @@ -33,7 +34,7 @@ public static class Bootstrap { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Bootstrap)); - public static readonly List ASSEMBLIES = new List + public static readonly List ASSEMBLIES = new() { "Prowlarr.Host", "Prowlarr.Core", @@ -51,72 +52,20 @@ public static void Start(string[] args, Action trayCallback = null Assembly.GetExecutingAssembly().GetName().Version); var startupContext = new StartupContext(args); - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var appMode = GetApplicationMode(startupContext); var config = GetConfiguration(startupContext); - switch (appMode) + if (appMode is not (ApplicationModes.Interactive or ApplicationModes.Service)) { - case ApplicationModes.Service: - { - Logger.Debug("Service selected"); - - CreateConsoleHostBuilder(args, startupContext).UseWindowsService().Build().Run(); - break; - } - - case ApplicationModes.Interactive: - { - Logger.Debug(trayCallback != null ? "Tray selected" : "Console selected"); - var builder = CreateConsoleHostBuilder(args, startupContext); - - if (trayCallback != null) - { - trayCallback(builder); - } + RunUtilityMode(appMode, startupContext, config); + return; + } - builder.Build().Run(); - break; - } + RunHostUntilShutdown(args, startupContext, appMode, trayCallback); - // Utility mode - default: - { - new HostBuilder() - .UseServiceProviderFactory(new DryIocServiceProviderFactory(new Container(rules => rules.WithNzbDroneRules()))) - .ConfigureContainer(c => - { - c.AutoAddServices(Bootstrap.ASSEMBLIES) - .AddNzbDroneLogger() - .AddDatabase() - .AddStartupContext(startupContext) - .Resolve() - .Route(appMode); - - if (config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true)) - { - c.AddLogDatabase(); - } - else - { - c.AddDummyLogDatabase(); - } - }) - .ConfigureServices(services => - { - services.Configure(config.GetSection("Prowlarr:Postgres")); - services.Configure(config.GetSection("Prowlarr:App")); - services.Configure(config.GetSection("Prowlarr:Auth")); - services.Configure(config.GetSection("Prowlarr:Server")); - services.Configure(config.GetSection("Prowlarr:Log")); - services.Configure(config.GetSection("Prowlarr:Update")); - }).Build(); - - break; - } - } + Logger.Info("Prowlarr has shut down completely"); } catch (InvalidConfigFileException ex) { @@ -135,6 +84,64 @@ public static void Start(string[] args, Action trayCallback = null NpgsqlConnection.ClearAllPools(); } + private static void RunUtilityMode(ApplicationModes appMode, StartupContext startupContext, IConfiguration config) + { + Logger.Debug("Utility mode: {0}", appMode); + + new HostBuilder() + .UseServiceProviderFactory(new DryIocServiceProviderFactory(new Container(rules => rules.WithNzbDroneRules()))) + .ConfigureContainer(c => + { + c.AutoAddServices(ASSEMBLIES) + .AddNzbDroneLogger() + .AddDatabase() + .AddStartupContext(startupContext) + .Resolve() + .Route(appMode); + + if (config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true)) + { + c.AddLogDatabase(); + } + else + { + c.AddDummyLogDatabase(); + } + }) + .ConfigureServices(services => + { + services.Configure(config.GetSection("Prowlarr:Postgres")); + services.Configure(config.GetSection("Prowlarr:App")); + services.Configure(config.GetSection("Prowlarr:Auth")); + services.Configure(config.GetSection("Prowlarr:Server")); + services.Configure(config.GetSection("Prowlarr:Log")); + services.Configure(config.GetSection("Prowlarr:Update")); + }) + .Build(); + } + + private static void RunHostUntilShutdown(string[] args, StartupContext startupContext, ApplicationModes appMode, Action trayCallback) + { + Logger.Debug("Starting in {0} mode", trayCallback != null ? "Tray" : appMode.ToString()); + + bool shouldRestart; + do + { + var builder = CreateConsoleHostBuilder(args, startupContext); + trayCallback?.Invoke(builder); + + shouldRestart = RunWithRestartCheck(builder.Build()); + + if (shouldRestart) + { + Logger.Info("Application restart requested, reinitializing host"); + NzbDroneLogger.ResetAllTargets(startupContext, false, true); + Thread.Sleep(1000); + } + } + while (shouldRestart); + } + public static IHostBuilder CreateConsoleHostBuilder(string[] args, StartupContext context) { var config = GetConfiguration(context); @@ -298,5 +305,20 @@ private static X509Certificate2 ValidateSslCertificate(string cert, string passw return certificate; } + + private static bool RunWithRestartCheck(IHost host) + { + var shouldRestart = false; + + var lifetime = host.Services.GetRequiredService(); + lifetime.ApplicationStopped.Register(() => + { + var runtimeInfo = host.Services.GetRequiredService(); + shouldRestart = runtimeInfo.RestartPending; + }); + + host.Run(); + return shouldRestart; + } } } diff --git a/src/NzbDrone.Host/ConfigureHostFilteringOptions.cs b/src/NzbDrone.Host/ConfigureHostFilteringOptions.cs new file mode 100644 index 00000000000..23d71895a23 --- /dev/null +++ b/src/NzbDrone.Host/ConfigureHostFilteringOptions.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Microsoft.AspNetCore.HostFiltering; +using Microsoft.Extensions.Options; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Host +{ + public class ConfigureHostFilteringOptions : IConfigureOptions + { + // Always allow localhost, loopback addresses and the hostname of the server, even if + // they aren't configured in the allowed hosts list so users can access the UI using them. + private static readonly string[] LoopbackHosts = { "localhost", "127.0.0.1", "[::1]" }; + + private readonly IConfigFileProvider _configFileProvider; + private readonly Logger _logger; + + public ConfigureHostFilteringOptions(IConfigFileProvider configFileProvider, Logger logger) + { + _configFileProvider = configFileProvider; + _logger = logger; + } + + public void Configure(HostFilteringOptions options) + { + var allowedHosts = AllowedHostsParser.Parse(_configFileProvider.AllowedHosts); + + options.AllowEmptyHosts = true; + + if (allowedHosts.Empty()) + { + options.AllowedHosts = new List { "*" }; + + _logger.Info("Allowed Hosts is not configured, accepting requests for any host"); + + return; + } + + options.AllowedHosts = allowedHosts.Concat(LoopbackHosts).Concat(GetServerHostNames()).Distinct().ToList(); + + _logger.Info("Accepting requests for hosts: {0}", string.Join(", ", options.AllowedHosts)); + } + + private List GetServerHostNames() + { + var hostNames = new List(); + + if (Environment.MachineName.IsNotNullOrWhiteSpace()) + { + hostNames.Add(Environment.MachineName); + } + + try + { + var hostName = Dns.GetHostName(); + + if (hostName.IsNotNullOrWhiteSpace()) + { + hostNames.Add(hostName); + } + } + catch (Exception ex) + { + _logger.Debug(ex, "Unable to get the hostname of the server"); + } + + return hostNames; + } + } +} diff --git a/src/NzbDrone.Host/ForwardedHeadersConfigurator.cs b/src/NzbDrone.Host/ForwardedHeadersConfigurator.cs new file mode 100644 index 00000000000..8445ae8a7d3 --- /dev/null +++ b/src/NzbDrone.Host/ForwardedHeadersConfigurator.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Common.Network; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Host +{ + public static class ForwardedHeadersConfigurator + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(ForwardedHeadersConfigurator)); + + public static void Configure(ForwardedHeadersOptions options, IConfigFileProvider configFileProvider) + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; + options.ForwardLimit = null; + + var trustedNetworks = configFileProvider.TrustedNetworks; + + if (trustedNetworks.IsNullOrWhiteSpace()) + { + return; + } + + foreach (var entry in trustedNetworks.Split(',')) + { + if (entry.IsNullOrWhiteSpace()) + { + continue; + } + + if (IPNetworkParser.TryParse(entry, out var address, out var prefixLength)) + { + options.KnownNetworks.Add(new IPNetwork(address, prefixLength)); + + Logger.Info("Trusting forwarded headers from {0}/{1}", address, prefixLength); + } + else + { + Logger.Warn("Invalid trusted network '{0}', forwarded headers from it will not be trusted", entry.Trim()); + } + } + } + } +} diff --git a/src/NzbDrone.Host/RestartableServiceLifetime.cs b/src/NzbDrone.Host/RestartableServiceLifetime.cs new file mode 100644 index 00000000000..22f10b28ba0 --- /dev/null +++ b/src/NzbDrone.Host/RestartableServiceLifetime.cs @@ -0,0 +1,249 @@ +using System; +using System.ServiceProcess; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; +using Microsoft.Extensions.Hosting.WindowsServices; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NLog; +using NzbDrone.Common.Instrumentation; + +namespace NzbDrone.Host +{ + public class RestartableServiceLifetime : IHostLifetime, IDisposable + { + private static readonly object StaticLock = new(); + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(RestartableServiceLifetime)); + private static WindowsServiceWrapper _singletonService; + private static bool _serviceBaseRunCalled; + + private readonly IHostEnvironment _environment; + private readonly bool _isWindowsService; + private readonly ConsoleLifetime _consoleLifetime; + private readonly TaskCompletionSource _waitForStartTask; + private readonly ManualResetEventSlim _hostStoppedEvent; + + private CancellationTokenRegistration _stoppedRegistration; + private bool _isDisposed; + + public IHostApplicationLifetime ApplicationLifetime { get; private set; } + internal TaskCompletionSource WaitForStartTask => _waitForStartTask; + + public RestartableServiceLifetime( + IHostEnvironment environment, + IHostApplicationLifetime applicationLifetime, + ILoggerFactory loggerFactory, + IOptions consoleOptions, + IOptions hostOptions) + { + _environment = environment; + _isWindowsService = WindowsServiceHelpers.IsWindowsService(); + ApplicationLifetime = applicationLifetime; + + if (_isWindowsService) + { + _waitForStartTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _hostStoppedEvent = new ManualResetEventSlim(false); + } + else + { + _consoleLifetime = new ConsoleLifetime(consoleOptions, environment, applicationLifetime, hostOptions, loggerFactory); + } + } + + public Task WaitForStartAsync(CancellationToken cancellationToken) + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(RestartableServiceLifetime)); + } + + if (!_isWindowsService) + { + return _consoleLifetime.WaitForStartAsync(cancellationToken); + } + + lock (StaticLock) + { + _singletonService ??= new WindowsServiceWrapper(_environment.ApplicationName); + + if (_singletonService.CurrentLifetime != this) + { + Logger.Trace("In-process restart detected, preparing host"); + _singletonService.PrepareForRestart(this); + } + + if (!_serviceBaseRunCalled) + { + _serviceBaseRunCalled = true; + Logger.Debug("Starting Windows Service"); + + var serviceThread = new Thread(_singletonService.RunServiceBase) + { + IsBackground = false, + Name = "Windows.Service.Lifetime" + }; + serviceThread.Start(); + } + } + + RegisterStoppedCallback(); + return _waitForStartTask.Task.WaitAsync(cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + if (_isDisposed) + { + return Task.CompletedTask; + } + + if (!_isWindowsService) + { + return _consoleLifetime.StopAsync(cancellationToken); + } + + try + { + _hostStoppedEvent.Wait(cancellationToken); + } + catch (OperationCanceledException) + { + Logger.Warn("Host stop was cancelled"); + } + + return Task.CompletedTask; + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!_isWindowsService) + { + _consoleLifetime?.Dispose(); + } + else + { + _stoppedRegistration.Dispose(); + if (_singletonService?.CurrentLifetime != this) + { + _hostStoppedEvent?.Dispose(); + } + } + } + + internal void CleanupForRestart() + { + _stoppedRegistration.Dispose(); + } + + internal void PrepareForRestart(IHostApplicationLifetime newApplicationLifetime) + { + ApplicationLifetime = newApplicationLifetime; + _hostStoppedEvent.Reset(); + _waitForStartTask.TrySetResult(null); + } + + private void OnServiceStart() + { + try + { + _waitForStartTask.TrySetResult(null); + } + catch (Exception ex) + { + Logger.Error(ex, "Service start failed"); + _waitForStartTask.TrySetException(ex); + throw; + } + } + + private void OnServiceStop() + { + try + { + ApplicationLifetime?.StopApplication(); + } + catch (Exception ex) + { + Logger.Error(ex, "Error during service stop"); + throw; + } + } + + private void RegisterStoppedCallback() + { + _stoppedRegistration = ApplicationLifetime.ApplicationStopped.Register(state => ((RestartableServiceLifetime)state)._hostStoppedEvent.Set(), this); + } + + private sealed class WindowsServiceWrapper : ServiceBase + { + public RestartableServiceLifetime CurrentLifetime { get; private set; } + + public WindowsServiceWrapper(string serviceName) + { + ServiceName = serviceName; + } + + public void PrepareForRestart(RestartableServiceLifetime newLifetime) + { + CurrentLifetime?.CleanupForRestart(); + CurrentLifetime = newLifetime; + newLifetime.PrepareForRestart(newLifetime.ApplicationLifetime); + } + + public void RunServiceBase() + { + try + { + Run(this); + CurrentLifetime?.WaitForStartTask.TrySetException(new InvalidOperationException("Service stopped without starting")); + } + catch (Exception ex) + { + Logger.Error(ex, "Service base execution failed"); + CurrentLifetime?.WaitForStartTask.TrySetException(ex); + } + } + + protected override void OnStart(string[] args) + { + CurrentLifetime?.OnServiceStart(); + base.OnStart(args); + } + + protected override void OnStop() + { + CurrentLifetime?.OnServiceStop(); + base.OnStop(); + } + + protected override void OnShutdown() + { + try + { + CurrentLifetime?.ApplicationLifetime?.StopApplication(); + } + catch (Exception ex) + { + Logger.Error(ex, "Error during system shutdown"); + } + + base.OnShutdown(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + } + } + } +} diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index 207ebd0240f..8ebfa9b60fd 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; using DryIoc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -33,7 +31,6 @@ using Prowlarr.Http.ErrorManagement; using Prowlarr.Http.Frontend; using Prowlarr.Http.Middleware; -using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace NzbDrone.Host @@ -54,20 +51,15 @@ public void ConfigureServices(IServiceCollection services) b.ClearProviders(); b.SetMinimumLevel(LogLevel.Trace); b.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); + b.AddFilter("Microsoft.AspNetCore.HostFiltering", LogLevel.Information); + b.AddFilter("Microsoft.AspNetCore.HttpOverrides", LogLevel.Debug); b.AddFilter("Prowlarr.Http.Authentication.ApiKeyAuthenticationHandler", LogLevel.Information); b.AddFilter("Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager", LogLevel.Error); b.AddNLog(); }); - services.Configure(options => - { - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; - options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); - options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); - options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); - options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7)); - options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10)); - }); + services.AddOptions() + .Configure(ForwardedHeadersConfigurator.Configure); services.AddRouting(options => options.LowercaseUrls = true); @@ -271,6 +263,7 @@ public void Configure(IApplicationBuilder app, } app.UseForwardedHeaders(); + app.UseHostFiltering(); app.UseMiddleware(); app.UsePathBase(new PathString(configFileProvider.UrlBase)); app.UseExceptionHandler(new ExceptionHandlerOptions diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index bdf3f5c1b5a..eb48a8e607b 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -181,6 +181,7 @@ private void GenerateConfigFile(bool enableAuth) new XElement(nameof(ConfigFileProvider.AnalyticsEnabled), false), new XElement(nameof(ConfigFileProvider.AuthenticationMethod), enableAuth ? "Forms" : "None"), new XElement(nameof(ConfigFileProvider.AuthenticationRequired), "DisabledForLocalAddresses"), + new XElement(nameof(ConfigFileProvider.AllowedHosts), "localhost"), new XElement(nameof(ConfigFileProvider.Port), Port))); var data = xDoc.ToString(); diff --git a/src/NzbDrone.Test.Common/Prowlarr.Test.Common.csproj b/src/NzbDrone.Test.Common/Prowlarr.Test.Common.csproj index 21b2c4a02ed..db685660cc9 100644 --- a/src/NzbDrone.Test.Common/Prowlarr.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/Prowlarr.Test.Common.csproj @@ -3,11 +3,11 @@ net8.0 - + - + diff --git a/src/Prowlarr.Api.V1/Config/HostConfigController.cs b/src/Prowlarr.Api.V1/Config/HostConfigController.cs index 4290baf2d81..998d82b2607 100644 --- a/src/Prowlarr.Api.V1/Config/HostConfigController.cs +++ b/src/Prowlarr.Api.V1/Config/HostConfigController.cs @@ -4,6 +4,7 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Network; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; @@ -37,7 +38,19 @@ public HostConfigController(IConfigFileProvider configFileProvider, SharedValidator.RuleFor(c => c.Port).ValidPort(); + SharedValidator.RuleFor(c => c.AllowedHosts).NotNull(); + + SharedValidator.RuleFor(c => c.AllowedHosts) + .Must(h => AllowedHostsParser.Parse(h).Any()) + .When(c => c.AuthenticationRequired != AuthenticationRequiredType.Enabled) + .WithMessage("Allowed Hosts is required when 'Authentication Required' is not 'Enabled'"); + + SharedValidator.RuleFor(c => c.AllowedHosts) + .ValidHosts() + .When(c => c.AllowedHosts.IsNotNullOrWhiteSpace()); + SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase(); + SharedValidator.RuleFor(c => c.TrustedNetworks).ValidIpNetworks(); SharedValidator.RuleFor(c => c.InstanceName).ContainsProwlarr().When(c => c.InstanceName.IsNotNullOrWhiteSpace()); SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Forms); @@ -116,6 +129,8 @@ public HostConfigResource GetHostConfig() [Produces("application/json")] public ActionResult SaveHostConfig([FromBody] HostConfigResource resource) { + resource.TrustedNetworks = IPNetworkParser.NormalizeList(resource.TrustedNetworks); + var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); diff --git a/src/Prowlarr.Api.V1/Config/HostConfigResource.cs b/src/Prowlarr.Api.V1/Config/HostConfigResource.cs index 4bd0cd10a26..c096ec8e63e 100644 --- a/src/Prowlarr.Api.V1/Config/HostConfigResource.cs +++ b/src/Prowlarr.Api.V1/Config/HostConfigResource.cs @@ -16,6 +16,7 @@ public class HostConfigResource : RestResource public bool LaunchBrowser { get; set; } public AuthenticationType AuthenticationMethod { get; set; } public AuthenticationRequiredType AuthenticationRequired { get; set; } + public string AllowedHosts { get; set; } public bool AnalyticsEnabled { get; set; } public string Username { get; set; } public string Password { get; set; } @@ -28,6 +29,7 @@ public class HostConfigResource : RestResource public string SslCertPath { get; set; } public string SslCertPassword { get; set; } public string UrlBase { get; set; } + public string TrustedNetworks { get; set; } public string InstanceName { get; set; } public string ApplicationUrl { get; set; } public bool UpdateAutomatically { get; set; } @@ -63,6 +65,7 @@ public static HostConfigResource ToResource(this IConfigFileProvider model, ICon LaunchBrowser = model.LaunchBrowser, AuthenticationMethod = model.AuthenticationMethod, AuthenticationRequired = model.AuthenticationRequired, + AllowedHosts = model.AllowedHosts, AnalyticsEnabled = model.AnalyticsEnabled, //Username @@ -75,6 +78,7 @@ public static HostConfigResource ToResource(this IConfigFileProvider model, ICon SslCertPath = model.SslCertPath, SslCertPassword = model.SslCertPassword, UrlBase = model.UrlBase, + TrustedNetworks = model.TrustedNetworks, InstanceName = model.InstanceName, UpdateAutomatically = model.UpdateAutomatically, UpdateMechanism = model.UpdateMechanism, diff --git a/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs b/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs index 4ff1e5b53f4..3fd49e0df22 100644 --- a/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs +++ b/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs @@ -9,7 +9,7 @@ public class DownloadClientResource : ProviderResource public bool Enable { get; set; } public DownloadProtocol Protocol { get; set; } public int Priority { get; set; } - public List Categories { get; set; } + public List Categories { get; set; } = []; public bool SupportsCategories { get; set; } } diff --git a/src/Prowlarr.Api.V1/System/SystemController.cs b/src/Prowlarr.Api.V1/System/SystemController.cs index e5ffb999602..9bc78378c7a 100644 --- a/src/Prowlarr.Api.V1/System/SystemController.cs +++ b/src/Prowlarr.Api.V1/System/SystemController.cs @@ -76,6 +76,7 @@ public SystemResource GetStatus() IsOsx = OsInfo.IsOsx, IsWindows = OsInfo.IsWindows, IsDocker = _osInfo.IsDocker, + IsContainerized = _osInfo.IsContainerized, Mode = _runtimeInfo.Mode, Branch = _configFileProvider.Branch, Authentication = _configFileProvider.AuthenticationMethod, diff --git a/src/Prowlarr.Api.V1/System/SystemResource.cs b/src/Prowlarr.Api.V1/System/SystemResource.cs index eead125cbf6..d0b82ee5790 100644 --- a/src/Prowlarr.Api.V1/System/SystemResource.cs +++ b/src/Prowlarr.Api.V1/System/SystemResource.cs @@ -25,6 +25,7 @@ public class SystemResource public bool IsOsx { get; set; } public bool IsWindows { get; set; } public bool IsDocker { get; set; } + public bool IsContainerized { get; set; } public RuntimeMode Mode { get; set; } public string Branch { get; set; } public DatabaseType DatabaseType { get; set; } diff --git a/src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs b/src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs index 7ec3b522053..3536748b291 100644 --- a/src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs +++ b/src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; @@ -27,7 +28,8 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte if (_authenticationRequired == AuthenticationRequiredType.DisabledForLocalAddresses) { if (context.Resource is HttpContext httpContext && - IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress)) + IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress) && + IsClientAddressKnown(httpContext)) { if (ipAddress.IsLocalAddress() || (_configService.TrustCgnatIpAddresses && ipAddress.IsCgnatIpAddress())) @@ -40,6 +42,11 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte return Task.CompletedTask; } + private static bool IsClientAddressKnown(HttpContext httpContext) + { + return !httpContext.Request.Headers.ContainsKey(ForwardedHeadersDefaults.XForwardedForHeaderName); + } + public void Handle(ConfigSavedEvent message) { _authenticationRequired = _configService.AuthenticationRequired; diff --git a/yarn.lock b/yarn.lock index d837ee8005d..4ffb630a66b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1465,6 +1465,11 @@ dependencies: "@types/node" "*" +"@types/redux-actions@2.6.5": + version "2.6.5" + resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.5.tgz#3728477ada6c4bc0fe54ffae11def23a65c0714b" + integrity sha512-RgXOigay5cNweP+xH1ru+Vaaj1xXYLpWIfSVO8cSA8Ii2xvR+HRfWYdLe1UVOA8X0kIklalGOa0DTDyld0obkg== + "@types/source-list-map@*": version "0.1.6" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.6.tgz#164e169dd061795b50b83c19e4d3be09f8d3a454"